How the importer works

Notes on the Flighty importer, for anyone building their own tool that writes Contrail records. This describes one implementation, not a specification.

Shape

Everything runs in the browser. There is no backend, so the CSV is never uploaded and there is nowhere for it to be stored. Sign-in is atproto OAuth using a client metadata document served from this domain, and the library holds DPoP-bound tokens in IndexedDB.

The importer requests three scopes and no others.

atproto
repo:com.airplaneian.contrail.temp.flight?action=create&action=update
repo:com.airplaneian.contrail.temp.trip?action=create&action=update

Create and update on two collections. It cannot delete, and it cannot touch anything outside those collections.

Column mapping

A Flighty export has 33 columns. These are the ones that become record fields.

CSV columnRecord field
Datedate
Airlineoperator, already an ICAO designator
FlightflightNumber
From, Toorigin, destination
Dep/Arr Terminal, Gateterminal, gate on each place
Canceled, Diverted Tostatus, diversionAirport
Eight time columnsthe out, off, on and in timestamps
Aircraft Type NameaircraftType
Tail Numberregistration
Seat, Cabin Class, Notesseat, cabin, notes
Flight Flighty IDsourceId
PNRtrip grouping only, never written

Seat Type and Flight Reason have no field to map to. The Flighty identifiers for airline, airport and aircraft type name entities the schema does not model, so only the flight identifier is used. source is set to flighty and relationship to passenger, since an export describes flights the account holder was on.

Times

This is the most involved part of the import. The time columns carry no offset, so the importer reads departure times as local to the origin and arrival times as local to the destination. The lexicon requires an explicit offset, so each one has to be resolved against the timezone that airport was using on that date.

The importer ships an IATA to IANA zone table generated from airportsdata. It does not ship a timezone database. The browser already has one with full historical rules, reachable through Intl.DateTimeFormat, so only the airport mapping is needed.

Resolving a wall time is not a single lookup. The importer collects the offsets the zone used a day either side, keeps those that read back as the same wall time, and classifies the result. One match is unique. Two means a repeated hour at a daylight saving fall-back, and the earlier instant is used. None means an hour skipped at spring forward. Both edge cases are surfaced in the preview rather than silently resolved.

Known bad data

Rows may carry a wrong landing time, which appears to affect older ones. Where it happens, the value in Landing (Actual) is the scheduled gate departure re-expressed in the arrival airport's local time. Compared as instants the two are exactly equal, so the check is an equality test rather than a heuristic, and the importer drops the value when it matches.

A flight number may be malformed. The importer checks each one against a plausible shape and flags it rather than correcting it, since a bad value is still what the source said.

Airport codes

The same table carries ICAO location indicators and FAA Location Identifiers, so the importer fills those alongside the IATA code the export supplies. Two things are worth knowing if you use that dataset.

It repeats the FAA LID in its icao column for US fields that have no ICAO indicator, so passing that column straight through publishes a wrong code. Comparing the two columns identifies them. Separately, most FAA LIDs are the same string as the IATA code, so the importer writes that field only where it differs.

Trips

Flighty has no trip concept, but legs booked together may share a booking reference, and the importer groups on it. Matching is case-insensitive, since the same reference can appear in more than one case.

The reference itself is never written to a record. A booking reference is six alphanumeric characters, a keyspace small enough that publishing a hash of one publishes the reference. The trip uses the first leg's Flighty identifier as its sourceId instead, which is already public as that flight's own sourceId.

Writing without duplicating

Records are matched on source and sourceId in the record body rather than on the record key. Keys are opaque TIDs, so correcting a flight number updates the existing record instead of minting a new one. Deriving a key from content would do the opposite.

The sequence is listRecords over the collection, index by source and source identifier, then applyWrites in batches of 100 with a create or an update per row. Trips go in a second pass, after re-listing flights to pick up the keys the server assigned.

On update, any field the importer does not own is carried through untouched, so sidecar data and fields from a newer version of the schema survive. createdAt is kept from the original record, since it records when the record was first written.