Skip to content

Flights

Flights are TravStats’ oldest domain. There are five ways to get one into the database, ranked by how much typing they save:

MethodTime per flightBest for
Drop a confirmation email or PDF~5 sFuture flights (booking → trip prep)
Scan a boarding pass~10 sAt the gate or right after landing
Spreadsheet round tripseconds for thousandsMass edits, adding rows in a table
List import (FR24 / any CSV)seconds for thousandsMigrating from another logbook
Manual entry~30 sOld flights, no email/pass available

Flights → Add Flight opens a step-by-step form:

  1. Departure / arrival airports — type IATA (FRA), ICAO (EDDF), or city (Munich); the autocomplete searches all ~9000 airports seeded on first boot.
  2. Date and times — local times in each airport’s timezone (TravStats stores everything as canonical UTC behind the scenes; v1.2.0 fixed a long-running bug where reminder emails fired 1–2 h off because of mixed conventions).
  3. Airline — IATA (LH) or ICAO (DLH) plus the operator name. Auto-fills if the IATA matches a known airline.
  4. Aircraft — IATA-T type code (32A for A320neo, 744 for 747-400) or freeform.
  5. Flight number — normalised to LH123 style automatically; case and whitespace don’t matter.
  6. Optional fields — booking reference (PNR), seat, cabin class, category, price with its currency, the trip, tags. All searchable later. Seat class and category start empty — an untouched form does not classify a flight as an economy business trip; a default can be set in the flight settings, including an explicit “no default”.

Distance is computed from the catalogue’s airport coordinates; you don’t enter it yourself. Duration is owned by the database and cannot go stale: whichever path writes the timestamps, the number follows.

Historical flights with no precise time (DATE_ONLY)

Section titled “Historical flights with no precise time (DATE_ONLY)”

For old logbook entries you remember as a date and a route but not the actual departure time, tick historical flight (route only) in the form. The date input switches to a Year + Month + Day dropdown (since v1.5.0); year-only and year-plus-month entry continue to work for entries where you don’t even remember the day. The historical status itself is derived from that choice and shown as a read-only pill — it is not something you pick from a list.

Giving a historical flight its times (since 2.6.1). The edit dialog carries the same checkbox. Untick it and the date and time fields appear: a known day is kept, a year-only or year-and-month date is cleared rather than turned into a made-up 1 January, and the save is refused until both clocks are typed — no midday is invented on the way out. The status then follows the times, as everywhere else. Ticking the box on a flown flight is the reverse move.

Flights saved this way carry depTimeSemantics: 'DATE_ONLY' internally. The UI shows them with a placeholder noon time, but the placeholder clocks are never counted as flight time: figures that depend on time of day (layovers, midnight crossings) skip them, and the flight’s duration is estimated from its coordinates and labelled as an estimate wherever it is shown. See Statistics → Which entries count.

Trip auto-detection sorts DATE_ONLY entries by chain coherence (arr → dep airport matches) rather than raw timestamp order, so a same-day round-trip whose anchor and return-leg placeholder times happen to be reversed still groups correctly. The chain-coherent sort landed in v1.5.0-rc.7 (issue #104). Older versions sometimes orphaned the return leg of a manually-entered DATE_ONLY round-trip.

The fastest path for booked-but-not-yet-flown flights.

Flights → Add → Import a document — paste the email body or drop an .eml / .msg / .pdf file. The same drop zone takes a cruise or a hotel confirmation; the server decides what it is looking at. For a flight, the pipeline (Ollama-first when configured — strongly recommended):

  1. Clean the email body and detect the sender / subject for routing.
  2. User template if you’ve recorded one with confidence ≥ 80 % — runs first.
  3. Ollama LLM (recommended primary parser) — your local LLM, default gemma3:12b. Handles multi-flight bookings (round trips, connections, group itineraries) reliably across any airline. The booking total and its currency land on the booking, not on each leg.
  4. Built-in regex templates as the fallback for eight European carriers when Ollama is unavailable: Lufthansa (LH, plus the older “Buchungsdetails” format), Swiss (LX), Austrian (OS), Brussels (SN), Ryanair (FR), easyJet (U2), Eurowings (EW), Wizz Air (W6). Single-leg bookings on these carriers parse fine without Ollama. Multi-flight emails are the regex layer’s weak spot — if you fly multi-leg itineraries, run Ollama.
  5. Generic regex extractor as last resort.
  6. The evidence rule, applied to whatever any provider returned: a booking must carry a flight number or both ends of a route, and a booking reference must be labelled as one. A date is not evidence — every marketing mail has one — so an airline promotion yields nothing rather than three empty flights.
  7. Review screen before anything is saved. You can edit any field, delete suggestions you don’t want, or hit Save all.

A mail in which nothing was found is an answer, not a server fault: the dialog says so and offers manual entry.

For airlines you fly often that aren’t on the built-in list and where you’d rather not depend on Ollama, you can record a user template — paste an example email, mark which lines contain which fields, and TravStats will reuse the rules on future emails from the same sender.

Common cases and what to do:

SymptomCauseFix
Aircraft type missingAirline doesn’t include it in the emailManually edit, or enable AirLabs/Aviationstack enrichment for auto-fill on next save
Wrong departure dateEmail has multiple legs, parser picked the firstReview screen lets you delete the unwanted ones before saving
Codeshare flight numberOperator vs. marketing carrier mismatchThe flight-lookup API resolves codeshares — set an AirLabs/Aviationstack key in Admin → Settings
All fields blankEmail is OCR’d from a forwarded photo, or HTML is heavily styledTry uploading the original .eml file instead of pasting body text

Flights → Add → Boarding pass takes a photo or uploaded image.

The barcode is read first. A decoded PDF417 or Aztec code wins every field it carries; text recognition keeps the fields no barcode holds (gate, terminal, boarding group). Only when there is no readable barcode does the vision cascade take over — Ollama with a vision model, OpenAI or Claude if a key is set, Tesseract OCR as the last resort — and whichever extracts a usable flight number wins. If nothing works you get a manual-entry form pre-filled with whatever text could be read.

Once there is a flight number and a date, the flight-lookup API fills in the aircraft, distance, scheduled times and operating carrier. The airline is reported by its printed name, not its two-letter code. Details on Boarding pass scanner.

A flight carries around 58 fields and the table shows nine columns. Clicking a row opens /flights/:id — the flight’s own page — where seat, gate, terminal, booking reference, companions, baggage allowance, overflown countries and everything else is readable without putting the record into an editable state. Edit is a button on that page.

Edit any field after the fact: click a flight in the list, change, save. Distance recomputes when you change either airport. The audit trail in Admin → Logs tracks who changed what, when, and what the previous value was. The flight list panel also lets you duplicate a flight (handy for a recurring commute — it opens a pre-filled copy to tweak) or delete it outright.

Bulk edits — Flights → Filter, select rows, Bulk edit. Useful for one-time corrections like:

  • Renaming an airline (merger or rebrand: “Air Berlin” → “AB”)
  • Replacing an aircraft type (typo: 744 → 74H)
  • Adding a tag to all flights of a trip
  • Deleting test flights from a demo run

If you re-import a boarding pass for a flight you’ve already entered, TravStats fills in missing fields on the existing row instead of creating a duplicate. Same flight number + same calendar day + matching airports = match. The dashboard surfaces a flightMerged toast that reports how many fields were filled in.

To force a merge explicitly via the API:

Terminal window
curl -X POST https://travstats.example.com/api/v1/flights?merge=true \
-H "Authorization: Bearer ts_pat_…" \
-H "Content-Type: application/json" \
-d '{ "flightNumber":"LH401", "departure": …, "arrival": … }'

The merge respects manual edits — fields you’ve curated never get overwritten. Only blank fields are filled. The same rule holds for the enrichment providers: a provider may not rename the carrier you flew with — on a codeshare the marketing and the operating carrier are two true answers — so your own text wins and an empty column is still filled. The full request/response schema is in the OpenAPI spec on your instance.

Changes a provider proposes for an existing flight do not land on the row directly; they wait as proposals in the Inbox. A flight that landed without its actual times is asked about one last time, through the one provider that answers for a past date — exactly once, values filled and never overwritten.

Not every flight is a scheduled A-to-B hop. TravStats models these as a flight subtype (not a separate travel domain), reachable from Add Flight → Special flight:

  • Sightseeing — a local loop where departure and arrival are the same airport.
  • Event — an eclipse chase, rocket-launch viewing or aurora flight, each with its own event coordinates and label.
  • ZeroG — a parabolic flight, storing the pattern centre, parabola count and provider.

They live alongside your regular flights in the same list, map and statistics, and can be edited or deleted like any other flight.

The Flights tab of the dashboard offers five views of the same data:

  • Routes — great-circle arcs on a 2D world map. Each airport pair collapses to a single arc (FRA–MUC and MUC–FRA are one line). Colour is a mode you pick — by frequency along a grey → amber → orange → red ramp, by status, and others — and the legend follows it.
  • Heatmap — density by airport
  • Airport frequency — proportional markers sized by how often you’ve used each airport
  • Trips — animated playback of routes through time
  • Globe — a spinning 3D globe (MapLibre + deck.gl) tracing the same arcs

The flight list and the dashboard table are one family with the cruise and lodging lists: status pills, sortable column headers, a column picker, one filter bar, a summary line for the rows shown (marked “filtered” when a filter is narrowing them), and a sort order that is remembered. Newest first, everywhere; an entry with no date sits at the bottom.

Plus statistics across the lot: total distance (”× around Earth”), total time, countries visited, top airlines, top aircraft, top airports — see Statistics.

Add free-form tags (business, family, solo, expat-life, 2026) on any flight. The filter sidebar lets you mix tags, airlines, aircraft, year and country to narrow the dataset for both the map and the statistics — handy for “all family trips in 2026” kind of slices.

Every save kicks off the achievement engine. Distance milestones, country counts, route patterns, equator/antimeridian crossings — the flight share of the catalogue of 270+ badges is checked on every flight create / update / delete and unlocked retroactively. You’ll see a toast on the dashboard when a new one fires. Only flown flights count; a booking unlocks nothing.

A flight proves a country by its local calendar days: arrival and departure on different days is a night slept, the same day a visit, and a change of planes with nothing else a connection — the one rung left out of the headline by default. A day trip is not a connection: MUC–FCO–FRA is a day in Rome. See Countries & passport.