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 column | Record field |
|---|---|
| Date | date |
| Airline | operator, already an ICAO designator |
| Flight | flightNumber |
| From, To | origin, destination |
| Dep/Arr Terminal, Gate | terminal, gate on each place |
| Canceled, Diverted To | status, diversionAirport |
| Eight time columns | the out, off, on and in timestamps |
| Aircraft Type Name | aircraftType |
| Tail Number | registration |
| Seat, Cabin Class, Notes | seat, cabin, notes |
| Flight Flighty ID | sourceId |
| PNR | trip 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.