# Changelog All notable changes to TravStats are documented here. Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## [Unreleased] **Before upgrading to 2.8.0-rc.28:** take a backup. Thirty-three new migrations since 2.7.0: two tables for the TravTrack diagnostics log, new columns on the admin and user settings for the ADS-B providers and the AeroAPI plan, two tables that count AeroAPI calls and track fetches, and the external-services policy (a policy column on the admin and user settings, a table for stored test results, a switch for a local custom AI endpoint), and the columns that receive the one-time copy of the service environment variables, a table for place captures from the Companion, the per-area reminder preferences (filled once from each account's current reminder settings), and one that clears the favourite airline "Lufthansa" that earlier versions stored as a shipped default (any other airline you chose stays), and one that replaces seven single-column user indexes covered by wider ones and adds an index for TravTrack point sync, and one that adds search indexes for the airport search, and two for this candidate: tags on lodging houses, places, roadtrips and tours, and a share-link kind for a single cruise, stay or place visit, and five for this candidate: the mail-intake tables (forwarding address, mailboxes, allowed senders, fetched mails), a share key for roadtrips, the members and notices of a TravPlan, the guests of a TravPlan, and the simplified federation setup (a findable switch per account and a statistics level per remote share), and four for this candidate: user connections between servers (a state, a generation and the sharing switches on each remote share), plan members over a connection, the plans you are a member of on another server, and an outbox for connection notices. And two for this candidate: the mode corrections of TravTrack legs, and the admin's choice of exchange-rate source. And one for this candidate: the place names you accept for TravTrack stops. And seven for this candidate: TravTrack fixed places and their settings, lodging rating categories (the existing room, breakfast and service ratings are copied into a per-stay map; the old columns stay), own place categories, the link between a train ticket and its flight (Rail&Fly) and its opt-out, and a column on the client write references that a later migration of the same candidate drops again. The TravPlan guest tables from rc.13 are dropped again with their rows — guests were never in a final release; no other data is removed. **Upgrading to 2.8.0-rc.15 — existing server connections ask to be confirmed again.** People who were already paired with someone on another server keep every trip shared so far, but nothing new travels until both sides confirm the connection once under Settings → Teilen and choose what it shares. Until then the statistics level of that connection is off, and leaderboard consents given to that person are withdrawn; both are set again when confirming. **Upgrading to 2.8.0-rc.5 — external-service settings move out of the environment.** On its first start this version copies every external-service variable still set in the server environment (`AIRLABS_API_KEY`, `AVIATIONSTACK_API_KEY`, `OLLAMA_URL`, `OLLAMA_MODEL`, the Immich, Dawarich, Strava, WebDAV, federation and the other service variables) into the instance settings, encrypted, wherever the instance field is still empty, and then never reads them again. The log names the variables it copied, never their values. From then on these services are configured only under Administration → External services; database, secrets, ports, time zone, logging, backup paths and first-boot defaults stay environment variables. One new migration adds the columns the copy needs. **Upgrading to 2.8.0-rc.6 — check your reminders.** Reminders are now set per area (flight, stay, cruise, rail, bus, ferry) under Settings → Benachrichtigungen. The upgrade fills them once from what each account had: an area that sent e-mail or push keeps doing so. Each area now has one list of lead times, taken from the e-mail switches (24 h / 2 h) where e-mail was on: an account that e-mailed only the 24-hour reminder but also had the 2-hour push keeps 24 h on both channels and loses the 2-hour push until it is ticked again. Nothing that was off is switched on. A stay's reminder is now counted back from its check-in time (15:00 in the stay's time zone when none is set) instead of firing at 08:00 on the day. **Upgrading to 2.8.0-rc.27 — an ordinary account's own Immich or Dawarich server must be public.** A connection that a non-admin account set up for itself may no longer point at a private, loopback or link-local address; such a URL is refused when saved and every request using it fails with a clear "private address" message. The admin's global connection, a connection from the environment and an admin's own connection keep LAN access. An admin who wants a user on a LAN Immich sets it up as the global connection. ### Added - **TravTrack has a new map page.** The map fills the width under a time bar (previous/next day, date, Tag · Gestern · 7 Tage · Monat · Jahr · Eigener, the day's coverage, last seen); layers, the day in numbers, the day log with legs, stops and logbook, and the replay sit in cards over the map, and a picked leg or stop opens its detail in the day log. On an iPad in portrait the cards fold away and the day log becomes a sheet. Export and deleting a range moved to the settings. - **TravTrack is its own settings page**, replacing "Standort": recording and devices, fixed places, privacy zones, recognition and suggestions, sources and imports, export, deleting a range and the diagnostics. Old links to "Standort" land there. - **TravTrack knows fixed places.** Home (from your residence, for the day shown), work and any number of other places, each with a radius and an optional weekly time window, plus — when switched on — the lodging you booked, from check-in to check-out. Everything recorded inside such a place shows as one stop named after it; the points themselves stay stored. Edit them on the settings page with a small map, or in the Companion. - **Rail and bus segments in a flight booking become rail and bus rides** (Lufthansa Express Rail, AIRail, Express Bus and the like): they leave the flight kilometres, the flight CO₂ and the passport's airports, and stay in the trip. A flight already stored that is such a segment is offered in the inbox for conversion — never converted silently; converting moves its documents, carries taxes and fees into the price, and either completes in full or changes nothing. - **A train ticket links to its flight (Rail&Fly).** An imported ticket that ends at the departure airport, or starts at the arrival airport, within a day of the flight is linked to it, whichever of the two arrived first; both pages show the link. - **Lodging ratings have categories you choose.** Ten built-in categories can be switched on and off, and you can add your own; switching one off never rewrites an older stay's overall rating. The statistics show each category on its own, the spreadsheet import and the trip file carry them, and "Nichts zu beanstanden" now asks for at least three rated categories, all at 5. - **Places can have your own categories** (Schwimmbad, Freizeitpark …) with an icon; deleting one moves its places to "Sonstiges" and says how many. - **Roadtrips edit more easily.** A live nights count with a chip per station, a station added by clicking the map (named from the place search, or said so when nothing is found), road or ferry per leg, a warning before unsaved changes are left, starting from a lodging, a GPX file or a Google or Apple Maps link, a round-trip switch, campsites in the station search, a GPX export and the weather per station on request. - **A package booking without a template can be read by the AI on request.** "Mit KI lesen" on a kept PDF turns it into a trip proposal you review before anything is saved; booking references, flight numbers, hotels and prices that are not in the document are dropped and listed. - **Mit wem unterwegs shows the kilometres flown together and the first and last journey together**, each linked to its entry. - **Flight insights show what the data services recorded:** time in the air from take-off to touchdown, the countries on the flight path and the codeshare flights with the airlines that actually flew them, each with how many flights it comes from. - **Cruise insights show money and ships:** cost per sea day, the most expensive cruise, spending per cruise line, the biggest ship and the average ship age at sailing. - **The spreadsheet export has sheets for trips, bookings, place lists and documents** (documents as a list only, never the files). - **Smaller additions:** afternoon departures, flights without a seat number, stays still planned and undated nights are now shown; a record for the longest ferry crossing; the place lists have their own tab next to Orte; an ordinary account can download a diagnostic package with its own data only. - **TravTrack (beta) shows a day as legs and stops.** The Track page lists each stretch of movement with its mode, distance, time and speed, and the stops between them; a leg can be picked in the list or on the map, which zooms to it. The mode comes from a logbook entry where one covers the leg, otherwise from the phone's motion, and reads "unbekannt" rather than guessing; it can be corrected per leg and reset to automatic. Points show their speed and altitude, range views carry distance, moving time and top speed, and the calendar flags days with a trip or a flight. - **Admins choose the exchange-rate source** (#391). Administration → External services offers Frankfurter (ECB) by default, AllRatesToday or a custom address, plus an optional fallback; each provider stays within the time left for the whole lookup. - **The import log is a table** with sorting, a column picker and paging; a run's date and contents stay visible in a narrow row. - **A roadtrip cost can be pinned to the leg between two stations** in the web dialog, as the Companion already could. - **TravTrack (beta) guesses a leg's mode from its speed when the phone sent no motion.** A steady walking, cycling or driving pace names the leg, always marked "unsicher"; train, bus, boat and flight still come only from a logbook entry or your correction, and a stretch that mixes paces stays unknown. Older days and imports benefit at once. - **The Companion can send how you moved.** With "Bewegungsart erkennen" switched on in the app, each recorded point carries the phone's motion class, and legs are corroborated by it instead of by speed alone. - **A TravTrack leg can be replayed and its points shown.** The leg card plays just that leg, shows its recorded points, and the map offers an accuracy filter and speed-scale presets (Zu Fuß, Rad, Auto, or scaled to the day). - **TravTrack (beta) suggests a name for each stop.** A stop in the day list shows its best suggestion — a known place or lodging within 100 m first, then the nearest real destination such as a charger, supermarket, restaurant, hotel, office or bank; streets, vending machines and parcel lockers are never offered — with "Übernehmen" to accept it and "Andere Vorschläge" to pick another. An accepted name is stored on the server, travels in the export, and can be removed again. - **Bookings can arrive by mail.** Settings → Mail-Eingang gives each account a forwarding address on the instance mailbox, or connects its own IMAP mailbox. Mails from allowed senders become proposals in the inbox — nothing is entered without your OK — and the original mail is attached to the entry it creates. Senders are checked by the receiving server's DMARC verdict, not by the From line alone. - **Servers connect once, people connect to each other.** An admin pairs two servers once under Administration → Instanz — by entering the other server's address and key, or by scanning its QR code with the Companion (the Companion confirms with the admin's password, and a TOTP code where set). After that, people on the two servers can send each other a connection request under Settings → Teilen (beta, behind the federated sharing switch); it takes effect only when both sides have confirmed. Each side chooses, before and after confirming, what it shares over the connection: trips offered to the other person, plans, statistics (off, overview or full), achievements and the leaderboard. Either side can end the connection at any time; a server whose key or address changes asks both people to confirm again, on both servers, and ends the plan memberships over that connection until then. - **Two servers on the same home network can connect.** Federation needs a public https address; an operator who runs two servers on one LAN can now list its address ranges in `FEDERATION_PRIVATE_PEERS` (for example `192.168.1.0/24`). Servers in those ranges may then use http, their own port and a bare IP address. Unset, nothing changes; loopback and link-local addresses stay refused either way, and a range that would include public addresses is ignored. - **Connected people can see each other's achievements.** With the achievements switch on for a connection, the other person sees the badges you have earned and your totals under Settings → Teilen → Verbindungen → "Erfolge ansehen" — nothing else. A settings change that could not be delivered to the other server within 14 days now asks both sides to confirm the connection again instead of being dropped silently. The admin QR code says that it does not expire and must be scanned again after a key or address change. - **Plans across servers.** A plan owner can invite a connected person on another server as a co-planner. The member sees and edits the plan from their own server and from the Companion — the plan itself stays on the owner's server, which checks every change — and can leave it again. A trip still offers "Auf anderem Server" to share it with a connection. - **TravPlan (beta) plans together.** A plan can have co-planners who may do everything except delete it, and a notice board. A ferry booking can be read straight into the plan. The guest link of rc.13 is gone: people on another server join a plan through a connection (above). - **Roadtrips can be shared** like the other areas, and travel along in the exchange file. - **External services live in one place.** Administration → External services and Settings → Services list everything TravStats does not do itself — flight data, maps and routing, photos and location, AI, open data, sync and mail — in one style. Each entry has an info panel (why TravStats needs it, what it costs, where to get a key, its quota, what leaves the server), a status that says whether it is switched on, set up and last tested OK, and which key actually answers: your own, the instance's or the server's. "Recommended" marks what gives the best experience for free. Standard hides only URLs and source order; Expert shows them. An admin "off" now stops a service for every account, including own keys, the server key and background jobs. AeroAPI's budget sits in its own row. - **Cloud AI needs each account's own consent.** An admin allows a provider for the instance; an account's documents go to it only after that account agreed, per provider and revocable. A remote Ollama and a custom endpoint count as cloud, and a changed address withdraws the consent. - **adsb.lol and ADS-B Exchange can name the aircraft of a flight in the air right now.** Both are off until an admin switches them on under Administration → External services (adsb.lol by a switch, ADS-B Exchange with its own key). They answer only for today's flights, fill registration and type, and never overwrite what another provider supplied. - **FlightAware AeroAPI knows its plan and its budget.** Admins pick Personal, Personal with feeder credit or a custom monthly budget; the instance key stops before the month's budget is spent and is rate-limited, while a user's own key never counts against it. Real flight tracks from AeroAPI after arrival are built in but stay off until the admin switches them on. - **TravTrack (beta) explains its gaps.** With the diagnostics log switched on in the Companion (Settings → Location, off by default), the phone sends what its recorder, its uploads and iOS did — never a position. Settings → TravTrack → Diagnose lines one device's day up against its stored points and names, for every gap of ten minutes or more, the cause the log supports: location access not "Always", recording paused, fixes dropped on the phone and why, an upload failed or still waiting, the app terminated, or "unexplained" — with a note when iOS was allowed to pause recording on its own. The raw log downloads as JSON; it is kept 14 days and can be deleted at any time. - **Setup can switch on the free services in one step.** The first-run page offers a switch that turns on the services that cost nothing and need no key — adsb.lol, rail timetables, open data (weather, elevation, Wikipedia) and airline logos — and says what leaves the server for each. The usage-statistics question stays its own question. - **A WebDAV backup can be restored from the share.** Administration → Backups lists the backups on the share; a downloaded one is checked against the checksum written at upload and then appears in the backup list like any other. Older backups without a checksum are accepted on size and marked as such. - **The server answers the Companion's new statistics:** ferry statistics, a trip's distance across every mode of transport, and the number of roadtrips. A trip's countries now include the places you visited on it, by the same rule as the passport. - **Reminders per area, with your own lead time and channel.** Each area can be switched on or off, reminds 24 h, 2 h or at a custom time (1–168 h) before departure or check-in, and goes to the app, by e-mail or both. "App" is a push through the server; the page says when push is not available on a device and why, and the Companion then reminds locally. - **Unnamed "Ort jetzt" captures from the Companion arrive in the inbox,** where they can be named, linked to an existing place or attached to a visit. - **Settings in one consistent shape.** Every page uses the same row and save model, the Standard/Expert switch states what each level shows and what the defaults are, the search finds settings by what they do, and the external-service forms test and save the same way. - **The dashboard map can hide routes per area.** A "Routen" switch in the map panel keeps only the markers — for flights, only the airports. It is also listed under Settings → Ansichten. - **New stays start at 15:00 check-in and 11:00 check-out,** in the stay editor and in a stay created from a roadtrip station. - **Download your own data.** Settings → Backup → "Meine Daten herunterladen" saves everything the account holds as one export file, the same one an admin export produces, without asking an admin. At most three downloads per hour. - **Stored details now show on the detail pages.** A flight shows its check-in desk, baggage belt, runway times, taxi and air time; a ferry its fare class and delay; a rental its supplier, broker, booking and contract number; a cruise its ship's build year, size and capacity; a tour its route notes. Rail, bus, ferry and rental show the converted price with its exchange rate, as stays already did. Empty fields stay hidden. - **Settings that did nothing now act.** The auto-update expiry hours set how long a pending flight update stays open (it was always 24 h), the cruise default line and cabin type fill a new cruise, "show cruise routes" hides cruise lines on the flat map, and the favourite airline fills a new flight (never an edited one). - **A stay reminder carries the stay,** so the Companion opens the right stay from the notification. - **What the Companion records now shows on the web.** Settings → Profil shows and edits your home place and passport expiry; a trip page shows its packing list and keeps it in step with the app (an edit made in the app meanwhile reloads the list instead of being overwritten). - **Ferry and rental catch up with the other areas.** Both forms take tags and pick companions from your list; the detail pages show the tags; a public trip link counts ferry crossings and rentals and draws their legs. - **Tracks, lines and places download as GPX or KML.** A recorded flight or cruise track, the stored line of a rail, bus or ferry ride, and your places (all, or one list) as waypoints. Only the owner can download; an entry without a line says so. - **Server features that had no button now have one.** Visit-photo suggestions can be refused ("Nicht diese") and offered again; visit photos can be linked to Immich from the Immich row under Services, as a job you can watch; pending flight updates can be taken or dropped all at once; flight enrichment lists its candidates and enriches one flight with a stated reason when it cannot; admins can set the instance's Dawarich connection, start the country-days sweep and reload the airport, airline and aircraft catalogues. - **Settings are ordered like the menu you read.** Account, display, the travel areas, trips, companions, loyalty, location, views, data, privacy and services; old links still open the right page. The location page uses the same rows as every other page, with the one-off tools folded away. - **Settings, rebuilt from the ground up.** One settings area for everyone; admins find the server's pages in the same menu ("Server · nur Admin") instead of a second admin area. Every setting is one row with a short name, at most one line below it and the rest behind "?". A change saves at once; if saving fails your choice stays, the row says why and offers "Erneut versuchen" or "Verwerfen", and nothing pending is lost when you leave the page. Groups that save together (profile, mail server, credentials) ask before you leave them unsaved. Dangerous actions (deleting, restoring, revoking every public link) say what is affected and how much, and some ask you to type a word. The search finds every setting, Expert ones too, without changing your level. All external services live on one Services page, one card per service, with your own rows on top and the server's below for admins. Old addresses keep working. - **Delete your own account.** Profil & Anmeldung → "Konto löschen" removes the account and all its data immediately, after your password (or a passkey older than a day) and, where set up, your second factor. The dialog offers to download your data first. The demo account and the last admin cannot delete themselves. - **One home with its history.** Where you live is the home of its period: moving starts a new period, and past trips keep counting from the home they started at. The Companion reads and edits the same record. - **Search everything from the top bar.** Press "/" to search flights, cruises, stays, places, rail, bus, ferry, rentals, tours and trips at once; hits are grouped by area and open the entry. - **Statistics across every way you travel:** longest run of days on the road, hours shifted across time zones, "not seen for N years" per country, airport and place, travel by season and weekday, price per km for rail, bus and ferry, nights per country, a Schengen 90/180 counter and photo statistics. Each figure opens the entries behind it. - **CO₂ for every mode,** from the UK government's 2025 conversion factors: per ride, per trip and per mode on the overview, "–" when a distance is unknown, a cruise as a lower bound, and an estimate of what the train saved against flying. - **TravTrack in the year review and badges:** days and kilometres recorded, top speed, highest point and time per way of moving (privacy zones and GPS spikes left out), four TravTrack badges, and the altitude in GPX exports. - **Import check-ins from Träwelling and travelynx,** and Google Timeline / Location History (old Records.json, semantic history and the new phone export), GeoJSON, KML and TCX into TravTrack. Files stay on your server; privacy zones and duplicates are handled like a GPX import. - **TravPlan route plans on the web:** create a route, compare variants, assign ideas to a day by drag or tap, see ten-year climate means per station, and hand a plan's stations over to the roadtrip, ferry and stays in one step. A handover keeps what the phone already wrote and only withdraws stops nobody touched. - **Tags on lodging houses, places, roadtrips and tours,** in the forms, the detail pages and the spreadsheet export. - **A public link for a single cruise, stay or place visit,** next to the existing trip link. - **Bus, ferry and rental join the rest:** a dashboard tab of their own, trip proposals, reminders (rental pick-up and return, roadtrip stations, planned place visits), badges with deeper levels (thresholds are a proposal) and forms that offer earlier entries as chips. - **Roadtrip and tour kilometres and roadtrip nights in the year review,** and a year recorded only by TravTrack now gets a year review of its own. - **Planned place visits in the calendar feed;** a rental shows the trip photos taken between pick-up and return; a house shows its stay documents; a visit's rating is editable on the web. - **Merge the copies of a duplicate cruise** from data quality, and assign existing flights to a booking from a trip's bookings panel. - **Maintenance can fetch a missing airport** and says whether the source was unreachable or does not know the code. - **The globe draws its dots, cruise paths, night shade and connecting lines as native map layers,** on the dashboard and on the trip map. - **Flight-track upload and the calendar subscription are out of beta** and available on every instance without the beta switch. - **The parser page explains itself:** what it reads, where an unrecognised booking goes (a link to the template workshop) and how to share a new template as a request. - **Settings search can show every hit,** not only the first eight; the data page opens with a jump list to Sicherung, Mitnehmen and Einlesen; the services page can show only the services that are active or set up. - **Flight updates in the inbox can be applied or rejected all at once.** The button acts on the updates currently shown, so a filtered list is handled in one step. - **A roadtrip's planned vehicle can be changed** after the trip exists, not only when it is created. - **Admins can re-seed the airport, airline and aircraft catalogues** from Maintenance and run the country-day sweep as a job that reports its progress, instead of waiting for the next start-up. ### Security - **A user-entered Immich or Dawarich address can no longer reach the server's own network.** An ordinary account could point its Immich URL at a server that redirected the photo proxy to internal addresses and read the answers. Private targets are now refused for ordinary accounts, redirects are never followed, the photo proxy passes only images and videos (no SVG) and drops a stream that stalls for 60 seconds. - **Changing or resetting a password signs out paired devices and API tokens.** Before, only browser sessions ended, so a device paired by whoever had the account kept working after the reset. Unused pairing codes are dropped as well. - **Unexpected server errors no longer show internal details.** A 500 now answers with a fixed message and a request id; the detail stays in the log. - **The login page no longer reveals the admin's e-mail address**, which anyone could read without signing in. Tokens are signed and checked with an explicit algorithm. - **Flags, certificate fonts and map styles load from the server itself.** Flags came from flagcdn and the certificate's fonts from Google, which sent every visitor's address to them without consent. Dependencies with known high-severity issues were updated, and CI now fails on high-severity audit findings and scans commits for secrets. - **Registering a passkey needs a fresh password check, and a young passkey cannot confirm a deletion.** Adding a passkey now asks for the password again, and a passkey younger than a day cannot confirm an account deletion; a backup code confirms either the way a password does. ### Fixed - **A fixed place's radius and schedule wrap on narrow rows**, and the /track time bar stays on one row on an iPad in landscape; the privacy-zone heading comes before its explanation. - **A TravTrack day spent in one place shows that stop.** A phone at rest sends few positions, and a silence of more than two hours cut the day into single fixes too short to count as a stop; such a day showed none, even inside a fixed place. A silence is now bridged for stops — but only when nothing was removed inside it (privacy zone, pause, deleted range or a single deleted point), never across devices, and a name within 500 m of a privacy zone is not shown. - **The stay reader no longer names a hotel group the document does not mention.** It used to map brands such as Holiday Inn or Courtyard to their group on its own. - **An own flight template that reads only a route lets the next reader try**, as the generic reader already did. - **"Stunden in der Luft" was block time.** The figures from gate to gate are now called flight time; "time in the air" is the new take-off-to-touchdown figure. - **A TravTrack day recorded only coarsely shows its stops and legs**, with the mode left unknown instead of guessed. - **The lime list colour no longer looks like a roadtrip on the map.** - **"Tage unterwegs" counts train, bus and ferry days.** Rides were left out of it; a ride now counts its departure and arrival day on the stations' calendars, for each area that is switched on. The number rises for accounts with rides on days nothing else covered. - **TravTrack stop names respect the stay-suggestions switch.** With it off, a stop offers only your own places and lodgings nearby and asks no outside service, and the row says so instead of "no name found". - **A TravTrack day recorded only coarsely is drawn again.** Fixes worse than 1 km are left out only where finer ones exist on the same stretch. - **TravTrack diagnostics name a stop the app made itself.** A gap that starts with the Companion ending its own recording now reads "recording paused" with the app's reasons (plan verdicts, location access, foreground or background), in German or English. - **No page is wider than a 320 px screen.** The header search field was 13 px too wide on the narrowest phones. - **TravTrack's speed guess works on real drives.** Speeds now come from windows of at least 30 seconds instead of single fix pairs, so 20 m of GPS noise no longer reads as a sprint and stop-and-go traffic no longer hides a drive; a long stretch at walking pace stays unknown rather than being called a car ride. A leg's top speed is taken from the same windows, so GPX days and imports show one too. - **TravTrack no longer draws outliers.** Fixes with an accuracy worse than 1 km — a cell-tower guess kilometres off — are left out of the day's line and range views, as they already were from legs. - **A night's wandering inside a hotel no longer splits the stop.** Two dwells less than 100 m apart with nothing in between straying further are one stop, not two stops and a fake leg. - **A picked TravTrack leg is no longer hidden behind its card.** The map frames the leg beside the card at its real height, "Ganzer Tag" ends a leg's replay, and the speed legend uses the language's number format. - **A cancelled status and other danger red is readable on its tint** (4.5:1); the globe time scrubber no longer carries a label screen readers cannot attach. An accessibility check over 33 main routes now fails on any new violation. - **A place's local name no longer starts with a Latin copy of itself** when OpenStreetMap lists several scripts. - **Demo cruise history follows the current year** instead of fixed years. - **Stale figures after an edit are gone from the web pages** whose memoised values missed a dependency. - **"Next flight" picks the flight that really departs next.** It compared the stored departure with the current time; for flights from the time before real departure instants were stored, that put a flight west of UTC too early or too late. It now reads the real departure instant. - **A rail ride without a time leaves the spreadsheet as a date.** The export wrote it as a midnight date-time cell; it is now a date cell, matching how it is imported again. - **Asiana Airlines leaves Star Alliance on 17 December 2026.** An alliance membership can now end on a date. Flights flown before the end still count for Star Alliance achievements; later ones do not. - **The time-zone note no longer covers the sidebar.** It sits bottom right now. - **The tours list shows its count like the other lists**, including 0. - **Trip timeline tiles take the current domain colours** instead of the colours of 2.6. - **Roadtrip side costs appear in the statistics.** The travel account lists ferry, toll, pitch and fuel costs per year and per currency, in the same form as the roadtrip cost panel, and never adds currencies together. - **The connection settings look like the other settings pages.** Same rows and switches, the usual confirmation when you end a connection, framed invitations and error notices in plans, and one word for each thing ("Kopplung" for two servers, "Verbindung" for two people). - **The save hint no longer promises that everything saves itself** on a page that has a save button. - **One name for the travel types page:** every hint that pointed to "Bereiche & Farben" now says "Reisearten". - **The reminder settings no longer crash** when the server sends no row for one of the newer areas. - **A finished trip folds its packing list away,** and hides it when it is empty. - **The flight-time estimate history follows the account** instead of staying in one browser. - **Filling a flight only uses a hit on its own route,** and every airport source is asked before the page says it is unavailable. - **Bulk refresh counts AeroAPI as a historical provider,** and the OpenSky "nothing to test" answer says the service is not configured. - **Shutting down stops every scheduled job and closes the server cleanly.** - **Areas such as markets, parks and museums are always recognised during a stay scan.** The two separate switches are gone; recognition runs whenever the stay scan runs, and suggestions still need your confirmation. - **Large imports and busy editors no longer stall on badges.** An import checks achievements once at the end instead of after every 20 rows, and repeated edits queue at most one follow-up check. - **The airport search answers from an index** instead of scanning the whole catalogue on every keystroke. - **A cruise with many ports no longer fails to save.** Sea routes are measured before the save starts instead of inside it, where a long itinerary could run out of time. - **The tour map loads its routes in one query** instead of one per route. - **A companion typed but not confirmed with Enter is no longer lost on save,** in every form that picks companions. - **Reminders for a switched-off area say so.** Settings → Benachrichtigungen shows such an area as off with a link to Bereiche & Farben, instead of accepting a reminder that could never be sent. - **Rail, bus and ferry summaries count in the database** instead of loading every ride, and a PDF attachment is read once per import instead of once per reader. - **The dashboard map's route switch also hides the native route layer,** greys out the route-only controls while routes are hidden, and `/dashboard?panel=routes` opens the panel directly. - **Large logbooks draw the map with less memory and work:** the route cache is gone, the route store keeps a bounded hot set, and several per-frame recomputations were removed. - **Rental, bus and ferry no longer say "(Beta)",** and the TravTrack description names its real sources: Companion, GPX, OwnTracks, Overland and Dawarich. - **Settings without an effect are gone:** the live-tracking check interval and "only during flight" no longer appear (the API still accepts them). - **The airport lookup no longer hangs or hammers its source.** It times out, remembers misses and reports an unreachable source as such instead of "not found". - **A WebDAV backup that arrives cut short is refused** instead of being restored from a truncated file. - **A device without WebGL2 sees a message instead of a crashed map page.** - **A failed settings load no longer saves the defaults over your real settings.** The web page used to write its empty defaults back when the first load failed. - **A backup no longer finishes as "successful" after a write error,** and an archive whose output stream fails is aborted instead of left half-written. A rental's receipts are filed in the same transaction that creates the rental. - **Large backups and exports stream instead of being held in memory,** so they no longer exhaust the server on a big logbook. WebDAV uploads send an explicit length, a download gets its own side file, and a stalled transfer is stopped by a watchdog. - **Streamed exports strip private share keys** the way every other API response already did. - **A provider outage no longer ends a flight's live tracking.** The automatic update sweep treated a failed lookup as "flight finished"; it now retries, and it passes the departure airport so the right flight is found. - **Statistics no longer show a page of zeros when a calculation fails;** the page reports the error instead. The cruise series counts completed cruises, and eight separate copies of the "does this flight count" rule are now one. - **Admin settings no longer show every API key as "not configured" after a failed read.** - **Airline logos missing during a provider outage at start-up come back** instead of being remembered as "no logo" for a day. - **The nearest-airport search near a pole and with odd inputs** returns airports instead of nothing or an error. - **Lists the server cuts short now say so:** a trip's photo gallery shows "first N of M", and the trip list, Dawarich stays, Immich albums and the parse-log export report when they were truncated. - **Deleting a flight or cruise updates its trip's status and badges** in the same step. - **A failed load says so instead of looking empty:** the dashboard no longer offers "add your first flight" when flights failed to load, the all-domains map names the domain that failed, and the flight panel's trips tab, the cruise map, the up-next card and the settings page show an error with retry. A failed settings read no longer stores a false snapshot, and a blocked auto-save is reported. - **The globe draws every cruise,** also with more than 100. - **Flight times in the year report and the map panel follow the shared duration rule,** and the report marks a total that contains estimated legs. - **A failing Immich import stops polling and names its failure;** the ferry editor shows why a save failed. - **Tag suggestions include ferry rides and rentals;** a bulk flight lookup validates its input, uses your own keys and reports provider failures. - **External services no longer mention server variables, and no field shows a raw label.** The source-order line and every reference to environment variables are gone from both pages; fields such as the Ollama address and model have their German and English labels, and a test now fails on any field without one. AISStream has its own row. - **WebDAV sync and download no longer fail in the browser while the server is still working.** Both run in the background; the page follows their progress and names the reason when one fails — sign-in, storage full, timeout, unreachable, cut short or checksum mismatch. - **Lists the server cuts short say so on the page:** the trip list, a trip card whose countries may be incomplete, the Dawarich stay inbox, an Immich album and the parse-log export. - **Large logbooks load faster.** Trip, flight and statistics queries read only the rows and columns they need, the airport, airline and aircraft searches filter in the database, and the geocoder caches are bounded. A finished trip is now marked completed within the hour rather than within five minutes. - **Maps and long lists stay smooth.** Map layers are no longer rebuilt on every animation frame, the live slider and replay redraw less often, and the activity list shows 200 entries with "show more". - **Setup no longer redirects past a failed step** when the page was left before its timer ran out. - **The trip list searches and filters on the server,** so a trip beyond the first 500 is found, and "mehr laden" loads the next trips in the order the list shows. - **A settings change made just before leaving the page is saved,** and two quick changes can no longer overwrite each other. - **A pasted booking text that yields no flight no longer says it was a mail.** The import page named the wrong source after an empty parse. - **First journeys keep early trips.** The first-journey figures no longer drop early trips, and path countries fold country codes and names into one entry. - **A section created inside a trip keeps its tags** instead of losing them on save. ## [2.7.0] - 2026-10-29 TravStats 2.7.0 is the largest release since lodging became a domain. Web and app now share one look; every figure in the statistics can show the entries it is made of; documents and receipts live on the entry they belong to; forms suggest what you entered before; places, photos and the passport grew up; rail, bus, rental cars and ferries are their own domains; and TravPlan, which plans a trip ahead, and TravTrack, your own location history on your server, wait behind the beta switch together with roadtrips and tours. The forty audit fixes first written for the never-promoted 2.6.3-rc.1 ship here too, further down this entry. **Before upgrading:** take a backup. Coming from 2.6.3, the first start runs 166 migrations, one of which rewrites stored cruise statuses from `flown` to `completed`; it also recalculates stored cruise legs along the sea route and re-routes stored straight ferry crossings once in the background, which can take a minute on a large logbook. A database restored from before the update is the way back; the previous image alone does not know the new columns. From 2.7.0-rc.27 nothing changes: 2.7.0 is that release candidate, retagged. ### Added - **A shared trip's dates sync to every member; its name does not.** Moving a shared trip's period moves it for everyone on the trip, each member gets an inbox notice they can undo, and each member keeps their own name for the trip. - **TravPlan is switched on and off per account,** beside the domains in your settings, like any other area. Accounts that already plan with it keep it on. - **Country and continent badges count ground contact.** A bus or ferry end, a service area or a fuel station places you in a country and on a continent, each piece of evidence on its own continent; a service area or fuel station counts as ground contact only, never as a visit. - **Both odometer readings prove a finished roadtrip driven.** - **TravTrack shows as soon as an admin switches it on,** and says how data comes in. - **The "what's new" dialog ends with a note from the developer** after the 2.7 highlights. - **TravTrack (beta): your own location history on your server.** The Companion records into it; a map of days and ranges, explored areas, monthly values, stays, a flight panel with the altitude profile and the photos taken on board, imports from GPX and a Dawarich copy, privacy zones that delete and never draw what lies inside them. Behind its own instance switch and the beta key. - **Inbox questions from TravTrack show their stretch of track:** a small map, from–to, duration, distance and why the question was asked. - **Duplicates are merged from the inbox, with undo.** Keep both, merge into one (refused with a reason when the copies carry different recordings or trips), and a ferry crossing inside a trip is offered to that trip. Dismiss, keep and link can be undone for ten minutes. - **Statistics for one year.** Every statistics endpoint the Companion reads takes a year and says which year it answered for; the passport counts a country in every year with evidence for it. - **Tram and U-Bahn rides run on their own tracks.** OpenRailRouting is asked with a tram or subway profile instead of the mainline one, so a U2 ride no longer detours over S-Bahn tracks; without such a profile the ride keeps a straight line with a reason. Rides can be routed again, one by one or all at once by an admin. Setup: docs/features/rail-routing.md. - **The calendar subscription shows local times.** Flights and rides carry the departure and arrival time zone and the local time in the title ("✈ Jeju 12:05 → Gimpo 13:10 · KE1206"); the link prefers a host name over an IP and warns when Google cannot reach it; the Companion can add it to the phone's calendar. - **A setup checklist for server-to-server sharing** in Admin → Instance, and the trip's share menu says why sharing with another server is not available yet. - **TravPlan has its own switch** in Admin → Instance (still beta). - **A place's visits, redesigned:** planned visits first, past ones by year, undated last, dates in your format, trip, photos, documents and rating on the row. - **A rail ride knows whether it was a train, an U-Bahn, a tram or a bus.** The mode is shown and can be changed on the ride, and the rail statistics count trains only. Every ride stored before this version is a train. - **A bahn.de connection's U-Bahn, tram and bus legs are reviewed in place.** The import dialog names each leg's mode; U-Bahn and tram legs can be kept as rail rides of that mode or skipped, bus legs can be saved as bus rides or skipped. Settings → Daten remembers the choice for each kind ("skip" until changed). - **bahn.de stops are resolved from bahn.de's own station data,** so a stop named the way bahn.de prints it finds its catalogue station. - **Settings → Mitreisende.** One page for everyone you travel with: how often each person appears per area, rename (the name changes on every entry), merge two spellings of the same person after a preview of what moves, and delete people nobody is attached to. Merging is refused when both are linked to different accounts. - **"Mit wem unterwegs" on the statistics overview:** days, entries and countries shared with each companion across all switched-on areas, stays included, each figure with the entries behind it. A shared day counts once; paying an expense alone does not count as travelling together. - **Server connections exchange keys by file.** Admin → Server-Verbindungen hands out a connection file (a ZIP with this server's address, public key, fingerprint and a short DE/EN guide) and reads the other server's file into the form. An imported server is marked "Noch nicht überprüft" until its address and fingerprint have been compared with the other admin over a second channel, and nothing is stored before "Server hinterlegen"; a known server arriving with a different key is flagged. The manual fields stay, folded away under "Von Hand eingeben". - **Replacing the server key asks first.** When a key is already stored, "Schlüssel erzeugen" explains that servers holding the old key refuse this server until their admins check and store the new one. - **Ferry crossings have a route map, the trip's photos taken during the crossing and a chapter in the year in review.** The map draws the stored sea route, or a faint straight line between the ports where there is none. - **Bus rides have a route map and the trip's photos** taken during the ride. - **Ferry and bus departure reminders.** The existing 24-hour and 2-hour reminder switches now also mail before a ferry crossing or a bus ride, in the port's or terminal's local time — for a domain the account has switched on (both are still behind the beta switch), and only for a departure with a time of day. - **Lodging enrichment from OpenStreetMap shows what it found before it writes:** the matched house with its OSM link and distance, and per field what would be filled and what stays. Nothing changes until "Übernehmen"; if OpenStreetMap names a different house by then, nothing is written. The match is stored on the lodging unless it already carries a reference. - **Flight track upload reads FlightAware and Flightradar24 KML exports, KMZ and Flightradar24's CSV,** reads their altitudes in the right unit, and says why a file gives no track (still behind the beta switch). - **Rentals appear on the statistics overview** as their own card and count as an experience; they add no travel days and no countries. - **Rail is out of beta.** It shows whenever an account has the rail domain switched on, independent of the instance's beta switch. - **Deutsche Bahn share links.** A bookmarklet (Settings → Data → Import → Rail) takes a connection shared from bahn.de or DB Navigator (`bahn.de/buchung/start?vbid=…`) into the rail review dialog. bahn.de is asked from the browser on bahn.de itself, and the connection reaches TravStats in the address fragment rather than in a request to bahn.de's or anyone else's server; no ride is saved before review. - **Settings closer to the agreed prototype**: tabs, breadcrumb, domain colour dots, expert notes, search that switches to the expert level when a hit lives there, one-open-at-a-time service accordions, larger switches for touch, and a page for a switched-off domain that says how to switch it on. Admin geocoder settings moved to External services → Maps & place search. - **New account settings**: a default colour for new trips (automatic colours now rotate the same way however a trip is created), whether sea days count as cruise days, a "Live-Verfolgung" switch for the Companion's in-flight recording, and seat statistics on or off. - **WebDAV sync per account** (expert): the account's export goes to the user's own WebDAV share (e.g. Nextcloud) daily, weekly or on "sync now", gzipped, with a chosen number of kept versions; clean-up only ever deletes exports this account uploaded. The last result is shown with its reason; a run that uploaded but could not clean up says "partial". Ordinary accounts may use public https servers only; administrators may also use a share on their own network. - **Admin → Hotel chains** (expert): search, create, rename, edit loyalty programme name and brand colour, merge (every link moves along), and delete only when nothing hangs on the chain; a deleted chain does not come back with the next seed. - Normal accounts can see whether keyless instance services are switched on. - **Map cards for every ground domain.** Rail, bus, ferry and rental routes open a card with their own statistics — first and last journey, average duration with fastest and slowest, average delay, total distance with what it measures (air line, along the track, estimated sea route) — and, where one exists, a picture of the train, ship or car model with its credit. Stations and ports open a card with a Wikipedia photo and summary, departures, arrivals and top destinations. Hovering shows a compact profile. Pictures and Wikipedia text use the operator-logo open-data switch; an outage says so instead of looking like "no picture". - **Station, terminal and port names on the map**, spaced by the same label pass as airports and hotels. - **Trips shared across servers** (beta, `federatedTripSharing`): pair with a person on another TravStats server by invitation code, offer a trip, keep it in step both ways with conflicts shown, and end, pause or resume it. A server restored from backup, or a peer with a new key, catches up after confirmation. - **The Companion's trip recording reaches the server**: stored, drawn on the trip map with a day filter, exportable as GPX and deletable. - **Recorded flight tracks** (beta, `flightTrackUpload`): upload a GPX or KML export and see the flown line on the flight page and the maps. - **Calendar subscription** (beta, `calendarFeed`): a secret iCal URL for upcoming entries. - **Imports from other flight apps**: OpenFlights, AirTrail, Jetlog, Flighty and App in the Air exports. - **Passport corrections**: add, correct or exclude countries by hand. - **Settings have one page per menu entry**, with a Standard and an Expert level per account. - **Template workshop**: rail, bus, ferry and rental templates, learning from screenshots via OCR, and export for the community repository. - Inbox: per-user switches for trip and stay suggestions; stay cards show a map and the probable place. - Admin: a 24-hour import mode that lifts upload and import limits; the template repository URL is an instance setting. - A GPS jitter filter on every recording source before anything is measured; each recording says how much it left out. - **Compare with friends is a leaderboard of totals.** `/compare` ranks you and the people who agreed — kilometres, trips, time away, countries, places — per year or overall. Each person decides which figures the other may see, and a pending request shares nothing until it is accepted. Friends on other TravStats servers join through an invitation code; between servers only signed totals travel, never single trips. - **Server connections in Admin → Instanz.** The instance's identity and the servers it trusts are managed in one card; the key is generated there and never shown. - **Ferry crossings can be shared** with a trip and travel in the `.travstats` file. - **A trip shows its travellers with their sharing state** — linked, shares this trip, request open — and offers one action per person. A person who agreed but is not yet on the trip can be added from the share dialog. - **One "Teilen" button** in the trip head for a companion's copy or a public link. - **Dawarich can suggest whole venues beyond markets.** A separate opt-in adds shopping malls, parks, zoos, theme parks and museum grounds. Independent overlapping venues stay reviewable, and visits are recorded only after confirmation. - **Markets are suggested as whole venues.** With separate consent, Dawarich scans automatically fetch public OpenStreetMap market boundaries for coarse regions along your track. A visit across several stalls stays one suggestion, with original point IDs, boundary sources and visible coverage gaps. Cached maps, bounded requests and remembered decisions avoid repeated work and duplicate visits; only confirmation records a visit. Hidden Places pause scanning, and manual scans can review the last 7, 14 or 30 days. - **Dawarich can suggest stays.** With your consent, Dawarich location history is scanned for places you stayed. Each suggestion is reviewed, edited and accepted or rejected, and a rejection is remembered. A slow scan can no longer overwrite the result of a newer one, and a polling error disappears once a read succeeds. - **Buses are part of "Upcoming".** They appear with the same day-by-day countdown as trains. - **Ferry crossings are a journey of their own.** A ferry booked with a vehicle — operator, departure and arrival port with local times, crossing duration, booking reference, seat or cabin, vehicle dimensions, passengers, price and the ticket PDF — is now an entry like a rail or bus journey instead of a line in the expenses. Ports are found by search, a ticket can be previewed before it is saved, an optional AIS lookup shows the live position, and a crossing can be linked as a leg of a roadtrip. The domain sits behind the beta switch. - **A moment without a trip has a place: notes.** A note is a date (or a date and a time on the clock of the place it was written), a text, optionally a place and one photo — nothing else is assumed. Notes live under Mehr › Sammlungen, appear on a trip's timeline when their day falls inside it, and are **private by default**; a note switched to shareable is shown only where you already share a trip (to the members of a shared trip, read-only, without position or photo; on a public link only if you pick "Notizen"). Sending a note twice from the phone creates it once. - **A trip can be shared with a companion who has an account on the same server.** Each person gets a linked copy; changes to the shared facts — flights, stays, cruises, rail, coach and rental rides, stops — reach the others as a notice in the inbox and can be undone; price, seat, booking reference and notes stay private. Consent is asked first and can be withdrawn, and a copy can be detached. A single trip also travels as a `.travstats` file to another instance, through a proposal you review before anything is created. - **Public share links** for a flight, a trip or a year in review, behind an instance switch that is **off by default**. You choose the fields; nothing else leaves the server (a test per field proves it), the link expires (a flight link two hours after landing), can be revoked at once, never appears in a server log (the token travels in the URL fragment) and carries photos without location data: the stripper keeps a short allow-list of JPEG and PNG parts and drops everything after the image, so the second image and the motion-photo video a phone appends are gone too. - **Booking deadlines.** Free cancellation until, payment due and online check-in opening are stored on stays, rentals, bookings and flights; a deadline known only as a day stays a day ("bis Ende 12. Oktober") and never gets an invented time. Templates may carry them, and a re-read mail fills a deadline only where it is empty. - **Trip expenses grew up.** A payer (a companion or a name), the exchange rate with its source (bank, cached or typed by hand), receipts that can be attached while creating the cost, and 13 categories. A home-currency total counts only what has a recorded rate and lists the rest per currency. - **The inbox asks six more questions about your data**, as questions and never as verdicts: possible duplicates, entries that cannot both be true (a flight leaving before the previous one landed, two houses on one night), a missing flight number, an unknown airport, a trip without an end, entries outside the trip's days. - **Every statistic says how it is counted and opens the entries behind it** — flights, cruises, lodging, places, roadtrips, tours, rail, rental, coach rides, the year in review and the overview: unit, time and zone rule, source, coverage and what is left out, in one "So wird gezählt" under each block, and a tap on any figure lists the entries it stands on. Extremes list the entries they were taken from; every one of the 192 badge rules shows what its progress stands on, under a work budget and a rate limit of its own. - **The coach (bus) domain, behind the beta switch**: logbook, detail page, trip map and timeline, statistics tab, night buses in the night account, badges, dashboard overlay, spreadsheet round trip and sync. A ride is drawn **along the road** when the instance has a routing service: the line is fetched once when the ride is saved and frozen, its length becomes the distance, and "Luftlinie" appears only for a ride that has no road line. An instance without a routing service says so in the form and keeps the straight line; one click routes the rides saved before ("Alle Luftlinien-Fahrten über die Straße berechnen"). One daily budget (300 per person, 1,500 per instance, under OpenRouteService's free 2,000) covers every call, and the shared demo account never calls the provider. Setup: `docs/features/routing-provider.md`. - **Photos: a nightly scan and a review of many suggestions at once.** An opt-in switch in Settings → Externe Dienste (it says what leaves the server and how the last run ended), and in the inbox visit suggestions show place, time span and reasoning together; you accept, correct or reject several in one go, and one that a logged visit already explains is flagged, not hidden. - **Parsers moved out of the server.** Issuer readers (Berge & Meer, Booking.com, TUI Cruises, DB, Sixt, airlines) are template files in the template repository, read by one generic engine with a per-document time budget; the home country orders templates and never filters them. The template workshop derives templates for cruises, places and package tours from rows you mark. - **Package tours import as a trip**: the operator's mail or PDF is read into a trip with its flights, stays and total price — after a preview — and a package booking can be entered by hand (operator, travellers, booking day). - **Rental invoices and rental details**: single fee lines on an invoice (refuelling, tolls, cleaning, extra km, damage handling…) are shown beside the booked price and taken over one by one without counting the cost twice; the provider list is data, with logos where the provider's site has one; deposit, odometer, hand-over evidence by what it shows and a compact return card. - **Rail**: a journey with changes is one logbook entry, the change between two trains can be marked tight, seats and tickets per train read at a glance, a DB share link imports a connection (and says plainly why it cannot when bahn.de refuses), and the import review shows unread booking facts instead of a dash. - **Cruises**: river ships and river cruises, kept apart in the statistics (a portless river day is not a sea day), a day card with time in port and an all-aboard time, and a booking read again is compared with the stored itinerary change by change. - **Forms ask before they lose your input**: closing, Back and navigation ask when a form changed; a field error sits at its field with a jump to the first; a save is never sent twice, and a create whose answer was lost offers a reload instead of a blind retry. - **Help is reachable by tap, keyboard and screen reader**: one toggletip for every field help; keyboard focus shows a 2 px outline on every control and field; touch targets follow the pointer, not the width. - **Deleting an account removes its uploaded files.** Photos, documents, receipts and note photos go with the account; an hourly sweep moves orphaned files to a quarantine and deletes them after 30 days — and returns them if a row points to them again, so a database restore to an older point cannot destroy newer files. - **Creates from the phone's outbox are exactly-once** for rail, coach rides, expenses, tours and notes (`clientRef`); a push reminder can be muted per trip on each phone. - **A stale page repairs itself**: after an update a browser holding an old page reloads once instead of staying blank, and says so if that fails. - **One look for web and app.** The web app now uses the Companion app's colours, spacing and type, one heading per page, and a dark mode built from the colour tokens. Settings are split into groups — flights, cruises and stays each have their own page — and the admin area is a page of its own. - **Every figure explains itself.** Clicking a statistic or a ranking row opens the entries behind it with each one's contribution; what cannot be assigned is named, not dropped, and the panel can be shared as a link. The running year is compared with the same period last year instead of a whole year. - **Documents and receipts on their entry.** Boarding passes, hotel invoices and confirmations are stored as documents on the flight, cruise or stay they belong to; price, currency and booking code can be taken over from them, each value shown first. - **Costs that belong to no booking** — tolls, ferries, parking, fuel — on a trip or a roadtrip section, counted in the cost statistics. Deleting a section hands its costs to the trip instead of dropping them. - **Help while you enter.** Forms suggest seats, flight numbers, terminals, frequent-flyer numbers, rooms, board, tags and recent currencies from your own history; a new entry lands in the trip whose dates it falls into. A suggestion never overwrites what you typed. - **Loyalty programmes in one place** — airline, hotel and cruise programmes with number and current status under Settings → Loyalty programmes, one button per alliance, and stays, nights or flights per year that lead to the list behind. - **Trip suggestions in the inbox.** TravStats reads from your flights, cruises, stays and place visits when you were away and proposes a trip, or the trip the entries belong to; nothing is created until you accept. - **Places, passport and photos.** Places are a full domain with lists, checklists and a CSV import that also takes a Google Takeout export; photos hang on places, stays, flights and cruises and in the trip journal. - **The Excel export reads back** — edit the spreadsheet and import it again, or carry it into another account. TravStats installs as a web app. - **AI and open data, under the admin's control.** The trip summary states only what is in the data; an admin can switch the AI parser off entirely, use an OpenAI-compatible provider (a provider outside your network only with explicit consent), and switch on weather from Open-Meteo and descriptions from Wikipedia. Dawarich location history counts as evidence for a country. - **The Companion app** (beta): pairing by QR code; push notifications for flight changes and departure reminders, end-to-end encrypted through a relay an admin switches on, with FlightAware AeroAPI as an optional status source; and a sync change log that never silently overwrites a newer version. - **Behind the beta switch:** roadtrips and day tours with elevation profiles, rail as a domain of its own, rental cars as a domain of their own, recorded GPS tracks for cruises, and hotel enrichment from OpenStreetMap. - **A new demo account** — an invented traveller from the Rhineland with ten years of travel. A new installation creates it and switches the beta features on as it does. - **Add existing entries to a trip from the trip itself.** "+ Eintrag hinzufügen" on a trip's timeline opens a picker with one tab per enabled domain; an entry that sits in another trip says so before it is moved. - **A train ride with changes is one logbook entry** (rail, behind the beta switch): the list shows "Augsburg → Hannover → Bielefeld" as one row with the total duration and the number of changes, a page for the whole ride lists its trains, and each train keeps its own page. Statistics still count trains. - **A weight unit for the baggage allowance.** Settings → Units & formats has a kg/lb choice; an allowance typed as a bare number ("23") is shown with it. - **Reminder and account mails in the app's look**, with a plain-text part, light and dark, and more of the entry's facts (terminal, gate, seat, booking reference; a stay's address, nights and board). Reminders go out only for domains you have switched on; invitation and password mails are bilingual. - **Display settings follow you to every browser.** Colours, map look, the dashboard's domain filter, hidden statistics sections and table columns are stored with your account instead of only in one browser. Page size and the open map panel stay per device. - **Place names you can read, in every script.** A place found in Seoul, Moscow, Tokyo or Cairo is named in Latin script where OpenStreetMap has one, and the name on the sign is kept beside it ("Gyeongbokgung Palace · 경복궁") — stored with the place and shown in the places list, on the place page, in the trip and on the map. A name is stored exactly as you typed it; an older place whose two halves sit in one string is split only where the map names the local half on its own, so "CU 삼청점" stays one name. A place saved under its Latin name in a country with another script gets the name on the sign added where OpenStreetMap knows the same object. - **A visit finds its trip.** A place visit saved without a trip goes into the trip whose days it falls on, when exactly one trip does; with none or several it stays unassigned. - **iPhone photos (HEIC) can be attached** to trips, places and stays. The original is kept; browsers get a JPEG copy. Capture time and position are now read from every uploaded photo. - **Rental cars** (beta) record a licence plate and suggest the common providers; rail and rental lists use the same table, header and filters as the other logbooks, and the map settings cover tours, roadtrips, rail and rentals — rentals optionally without the connecting line. - **Rental cars** (beta) take the odometer reading at pick-up and return; the kilometres driven follow from them unless the invoice states its own figure, and a return reading below the pick-up one is refused. - **A seat reservation booked later finds its journeys** (beta). Importing a Deutsche Bahn reservation offers to attach coach and seat to the train journeys already logged, shows what would change, and asks when several journeys fit. - **Places nearby list the important ones first.** Picking a place near you shows a longer list with sights, stations and landmarks ahead of shops and benches, and a place search prefers results near where you are. - **The Companion can send a flight's recorded track and its observed takeoff and landing**; a reported landing marks the flight as flown at once, and the times arrive as suggestions to confirm. ### Changed - **The production dependencies carry no known vulnerability:** DOMPurify (critical, used for PDF export), postal-mime 4.1 (.eml previews), body-parser, multer, fast-uri and zod are updated. - **Rental cars, coach rides and ferry crossings are out of beta.** They show whenever you enable the area in your settings. - **Completing a stay from OpenStreetMap is out of beta,** with a stricter name rule: a town or landmark word alone no longer matches a house, and English and German names are compared too. Measured on 200 real stays: 1 wrong match in 156 (was 2 in 152). - **Detect places automatically** is on by default with the stay scan; its two area switches are expert settings. - **The export cuts privacy zones out of older recordings** (trip recordings and flight tracks from before TravTrack). Stored data and the admin backup stay complete. - The README and store texts describe TravStats as a logbook first, with optional recording through the Companion. - **The lodging import preview says what each row will do.** The action reads "Aufenthalt hinzufügen", "Hotel und Aufenthalt anlegen", "Weiteren Aufenthalt anlegen" or "Aufenthalt aktualisieren" instead of a bare "Anlegen"; a matched hotel is shown with its stored name and city as plain text, with the import's spelling beside it where it differs; the summary counts what will be added, skipped and still needs a decision, and the reminder to decide only shows while something is undecided. A guessed match says what it rests on (name and city, name only, a similar name, the same place) instead of a green "Passendes Hotel gefunden"; several possible hotels offer a choice; "Keine Koordinaten" is gone from rows that join a stored hotel. "Anderes Hotel? Zuordnung aufheben" replaces "Nicht dasselbe Hotel — neu anlegen", which never created anything. - **Recorded flight paths are off on the dashboard by default.** The map shows routes; a recording belongs to one flight. A new map option "Aufgezeichnete Flugwege zeigen (wo vorhanden)" draws them as thin lines on top of the routes, which stay visible even when every flight on a route was recorded. On the flight page the recording is shown over the dashed great circle. - Ferry crossings stored as a straight line are re-routed along the sea once, in the background, after the update. - An invitation code that is retried after it already connected reads as "already connected". - The end-to-end encrypted transfer moved to its own page, `/encrypted-share`, behind its own beta switch. - A shared trip is copied for each person on their own calendar: a copy's status (planned, under way, done) follows the recipient's time zone. - If the recipient already logged the same flight or crossing on another of their trips, sharing moves that entry into the shared trip and links it instead of creating a duplicate; the notice says so and can be undone. - A provider change that reaches every member through the shared trip makes one inbox item, not one per person. - Ferry edits and deletes accept an optional read version. Conflicting writes return HTTP 409; edits claim the version atomically in their transaction. Older clients can continue to write without a version precondition. - Settings expose supported travel domains independently of enabled domains, allowing compatible clients to offer a disabled domain again. - TravPlan uses shared theme tokens and the selected application locale for number formatting. - Full data export includes saved TravPlan content and proposals; daily quota counters are explicitly excluded. - **A sailed cruise is `completed`, not `flown`.** The flight word leaked into cruises; `flown` is still accepted on write and read as `completed`, and stored rows were migrated. - **Private files are served at addresses that do not end in an image or document extension** (receipts, profile pictures, admin log pages), because a proxy that caches by extension served them without asking the app. The old addresses redirect. - **Numbers follow the app's language**, never the browser's. - **A trip's entry count and the trip page include coach rides**; the trip export, trip merge, micro-trip cleanup, delete question, countries and the AI summary brief know them, and so does sharing. - **The what's-new dialog** is short again — eight items — and describes what 2.7.0 contains; a test keeps it from growing back. - **Cruise kilometres go up.** A cruise leg is now measured along the sea route the map draws; some legs used to count only as a straight line. Stored legs are recalculated once on the first start after the update. - **Restoring a backup from an older version works.** The restore empties the schema inside its own transaction, loads the archive and brings it up to this version's migrations before it reports success; a failed migration at startup now stops the container instead of starting on an outdated schema. ### Fixed - **Two members moving a shared trip's dates at the same moment both save.** One of the two saves used to fail; and an undo that raced a member's own save could overwrite it with the old dates. - **A replace import from a spreadsheet no longer starts a backup for the demo account or beside a backup already running.** - **Places count in every category they carry** except the transit kind (forgejo#299). - A booked or cancelled cruise counts in no cruise figure. - The Dutch Sixt invoice's fee lines are read (forgejo#280, forgejo#237). - The ferry port search finds the port of the town you typed. - The year filter applies to the route network and the route list in the statistics. - The profile shows your real username and no placeholder e-mail address. - Ferry crossings are offered in the map's add menu; the narrow rail list names each ride's mode and line; on a phone the TravTrack map sits right under the range control; an empty inbox shows one message with one action; flight-track gaps are drawn dashed. - **Uploading a document from the web works again.** Every web upload had failed with "No file uploaded" since September. - **A server no longer crashes when another server cannot be reached over IPv6** while checking it; addresses are tried IPv4 first. - A share link of a trip without dates ends after the trip's last entry. - Saving part of the display settings over the API no longer clears the time zone. - Station names of rail, bus and ferry on the trip map no longer print over each other; a cruise leg's hover shows its ports and sailings. - **A skipped row no longer fails the whole lodging import.** A skipped row with a cleared date or name made the server refuse every row; skipped rows are now sent without their fields. - **"Auswahl importieren" is only enabled for a preview the server will accept.** Names and cities over 200 characters, negative prices and stays over ten years are flagged on the row instead of failing the import, a price without a currency is flagged on updates as well, and a row set to add a stay with both dates cleared is flagged instead of creating nothing. - **Picking one of several possible hotels checks that hotel's stays for the same check-in,** and an update shows every changed field, an edited price or currency included. - **Importing the same bahn.de connection twice no longer saves its rides twice.** The repeat was looked up by the printed stop name, while the first import had stored the catalogue's name. - **Two buses or trains leaving the same stop in the same minute are no longer taken for one ride.** A ride counts as already logged only when its destination and its line or train number agree too; where one side names no line, the leg is kept ticked and marked "möglicherweise schon erfasst". - **A ticket with a booking number finds a ride saved earlier from a bahn.de connection without one,** and a busy day no longer hides the ride sought behind older rides at the same stop. - **A recorded flight is drawn whole.** Sparse fixes from a phone in the cabin were dropped as single points, so LH719 Seoul-Munich showed only its last third. Every fix is kept; recorded stretches are solid and the holes between them dashed. - A recording with holes no longer sets its distance against the great circle ("−60 %"), since it measures only its recorded stretches. - The start of a recorded flight is no longer hidden under the map attribution on the flight page. - TravPlan is refused by the server while the beta switch is off; before, its endpoints answered any signed-in client. - The import card promises AI reading only for the kinds of booking that have it. - The empty tours view no longer covers the list button. - A large KML file is read in linear time instead of blocking the server for seconds, and a very large one no longer crashes the reader. - Lodging enrichment never overwrites a value saved while OpenStreetMap was being asked. - **A Deutsche Bahn share link no longer reads as a failed import.** Such a link carries no booking reference, price or seat, so the review dialog now says so in one line instead of flagging every empty field as "not found in the document". - **A train bahn.de names by number alone reads "Zug 57031"** (EN "Train 57031") on map cards, in the trip overview, the review and the ride pickers, instead of a bare number. The number is stored as the train number; a category is kept only where bahn.de states one. Rides imported from a share link in rc.20 read the same way. - Bus lines are drawn on the globe. - A Wikimedia Commons photo on a map card carries its author and licence where Commons provides them; a non-free local photo is not shown. - "Last here" handles date-only rides, and no longer names an older visit as the latest when a newer arrival's date is unknown. - Close and toggle buttons on iPad map cards are 44 px; the AI trip-summary offer wraps on narrow screens. - Deleting an account also deletes its pending sync changes. - **A global API key is no longer shown to every user.** The key-status answer carried stored values; it now carries only whether a key is set. A key the instance cannot decrypt no longer counts as "active for everyone". - The trip overview lists ferry crossings, and the trip map draws rail, ferry and rental — on the globe as well. - Overnight journeys show their arrival day on cards and tooltips. - A date-only journey no longer gets an invented duration. - The "far from your destinations" hotel warning considers arrivals by rail, bus and ferry, not only flights. - Map labels no longer print over each other in "All" and "Important", on the globe (where they also keep clear of the time control) and on the trip map. - A failed route or rental load on a map says so and can be retried instead of looking like no data. - iPad touch targets in the menu, the recording panel and settings follow the pointer, not the width. - The statistics breakdown names ferries in both languages. - A settings search result opens its folded group. - Sharing records an entry's author only where it is known and checks a received change's author. - Public-link lifetimes are calculated from the server clock, preventing a fast browser clock from rejecting “Until landing (+2 hours)” or “90 days”. Explicit expiry timestamps remain supported for existing API clients. - Public-link permission is now under Administration → Instance. Personal settings retain link management and point admins to the instance switch. - **Backup retention keeps what it should, and a remote copy is deleted only when you say so.** Old backups were pruned by the wrong identity. A WebDAV delete that fails with 401, 423 or 500 now keeps both copies and the database row. - **Rail lines are re-fetched when their inputs change, and a failed fetch keeps the stored route.** The distance is shown consistently. - **A year-only or month-only flight stays in its year** — in the year summary, the time series, the passport and the flight's own times — instead of landing on 31 December of the year before at airports west of UTC. The precision is stored with the flight. - **Counting rules have one home.** The busiest day, "connection", the time-travel index (landing earlier than you left, date included), a trip's cost, a trip's duration, "new this year" (in your zone), the country distribution (an end without an airport code proves no country) and the aircraft registration (one hull, whatever its case) each follow one rule everywhere they appear. - **An empty value is shown as unknown, not as 0**: business rates, average flight duration, a rail tile without timed rides. Seats are placed by the cabin's own layout, or not at all. - **A Takeout re-import no longer repeats places**: a place is found in every spelling of its Google id, and a trip stop within 100 m on the same day is not added twice. - **First writes no longer race into a unique violation** (display preferences, place-list entries): two settings saved at the same moment on a new account. - **A fresh setup says it is set up**, the welcome hint no longer covers the empty-state buttons, and a session that only exists as a cookie signs in. - **Opening Settings no longer eats the limit for adding flights.** The bulk-refresh preview shared the flight-creation limit of 20 an hour, so about twenty visits to Settings → Konto blocked adding flights for an hour; it now has its own limit, and the card shows one state, in plain words and in the reader's language. - **One unreadable row no longer rejects a whole flight file**; the preview names the line and the reason. - **An expired reset link is said when the page opens**, with a way to a new one. - **The trip map frames a trip's coach rides** and does not call the trip empty; the trip card counts them; the trip export no longer leaves them out. - **Layout**: the admin pages, the cruise statistics, a lodging tile and the backup form no longer widen the page on a phone; the flights toolbar and settings selects wrap. - **Security and hardening**: a refused cross-origin request answers 403 and not 500; templates cannot impose unbounded regex cost on the server; the public share API is exempt from the origin list on purpose (it carries no cookie and answers no cross-origin read); dependency alerts that have a fix are patched. - **Backups**: an archive from an older version migrates inside the restore's own transaction, and a newer one is refused by name. - **Smaller corrections you can see**: a train ticket with a change of trains keeps every train; a day-only ride keeps its day in the form; the autumn hour that occurs twice keeps its occurrence on edit; a rental's actual return can no longer precede its actual pick-up; roadtrip station drafts survive a lost connection and are merged before they overwrite anything; overlapping stays are a notice on saving, never a block; two of your own places can be merged without losing a visit, photo or list; a hovered station, train or rental on the map says what it is; every table, dialog and list reaches the touch size on a coarse pointer. - **A flight is "flown" once it has landed.** It stayed "scheduled" for six hours after its scheduled arrival. A landing reported by the live flight data now counts at once, and a flight without live data turns an hour after its scheduled arrival; a delayed flight the live checks are still following is left alone until its arrival is recorded. - **A trip lasts as long in the statistics as on its own page.** A trip from the 15th to the 18th showed four days on its page and three as the average trip length, because the statistics counted nights. - **A scanned boarding pass keeps its seat and booking reference.** A barcode read in the browser filled flight and route, but left seat and booking reference empty although the barcode carried both. - **An Excel export in the other language can be imported.** An English export was refused as "no known sheets" under a German interface, and the other way round. - A roadtrip still underway calls its distance "Strecke · geplant" instead of "Gefahren", which counted legs that were still ahead. - A stay's page labels its per-night figure as the average of the total ("Ø Gesamtkosten pro Nacht"), so it no longer reads like the room rate. - A flight imported twice says it is already in the logbook and offers to open it, instead of asking you to check correct input. - German counts use the singular: "1 Flug", "1 Fahrt", "1 Miete", and on the roadtrip card "1 Nacht", "1 Station", "1 Tour". - Rail text import keeps operator, booking number, class and price. - The hotel and cruise import previews are reachable by keyboard; a dialog no longer loses its focus while it saves; every field of the flight review is named by its label. - A roadtrip that has not started no longer claims more days ahead than it has. - A plain flight confirmation keeps its booking reference and both times when read without an AI model. - The trip card counts rail and rental entries; the rental form accepts a German decimal comma. - **A first import of Deutsche Bahn tickets gets through.** The Online-Ticket layout DB prints since spring 2024 is read without an AI model; before, only tickets in the older layout were. Saving train journeys no longer stops after twenty in an hour (now 300), and reading PDFs no longer after twenty in fifteen minutes (now 60). - A Deutsche Bahn ticket in the new layout WITH a change of trains keeps every train, each with its coach and seat (rc.5 read only the first). - **A dialog no longer closes when a text selection ends beside it.** Selecting a field's text and letting go of the mouse outside the dialog closed it and took the form with it. - The flight list keeps the planned and the actual time side by side again; the actual time had wrapped onto a line of its own. - The stay reminder mail linked a page that does not exist. - The rail import asks the next AI provider when the first one does not answer. An Ollama address that was entered once and no longer runs used to make every ticket "AI parser not reachable" while a configured OpenAI key was never asked. - Every parser — stays, cruises, flights and the trip summary, not only rail — now asks the next AI provider when the first one does not answer. - **A flight's takeoff is known while it is in the air.** One live check now runs 30 minutes after departure; before, a long-haul flight's actual departure arrived only shortly before landing. - Hovering a train, a station or a rental on the map says what it is. - A rail or rental list that cannot load says so in amber with a retry, and the figures above those lists count the whole filtered list, not just one page. - **Restoring a backup from an older version** runs the missing migrations in the same transaction, so a failure leaves the database as it was; a backup from a newer version is refused, and the server no longer starts on a half-finished migration. - A request from a foreign web origin is refused with 403 instead of a server error. - The rail form labels the arrival delay and keeps it beside the arrival time. - The rental list names the car actually driven before the booked class, shows a status on every rental, and a rental returned where it was picked up is drawn as one station dot. - A Hawaiian place name with an okina (ʻ) is no longer treated as a foreign script. - **A train ride with changes is one ride.** It gets one reminder and one "next up" entry, named by where it ends ("Deine Fahrt nach Wien"), instead of one per train; a single train is named by its destination too. - The name on the sign is added only from the place itself: a longer map name counts only when it adds what kind of place it is ("Jongmyo Shrine"), not when it names another object ("Mount Fuji Weather Station"). - The Companion can tell an unknown flight from a server that does not offer the route: the flight routes answer `FLIGHT_NOT_FOUND`. ### Audit fixes first written for 2.6.3-rc.1 A second instance audited the tree from the outside and reported forty-two findings; forty are fixed here, along with a crash found separately. Nothing here adds a feature — it is the release where a number that was wrong becomes right, and where several doors that were open get shut. Three of the fixes change figures that are already on screen; they are named below. #### Security - **A receipt file belongs to whoever uploaded it.** Knowing another account's receipt URL was enough to obtain the file: permission was derived from "does this caller own something that points at this file", and a caller writes their own entries — so pointing at a stranger's receipt was enough to be allowed it. The uploader is now recorded at upload time, from the session, where no request can reach it. Existing receipts keep working. - **A stay, a cruise or a booking can only be filed under your own trip.** The server checked the hotel a stay was nested under and then took the trip, booking and loyalty card from the request without asking whose they were. One account could file an entry against another account's trip, and it then appeared on that person's own timeline. - **A read-only access token can no longer switch on two-factor or delete a passkey.** Those decide who gets in, and a token that may write travel data has no business making them. - **Two-factor is asked before a forced password change, on every door.** An administrator's forced password change simply did not apply to an account with two-factor switched on. - **Changing a password ends every other session.** The sign-in cookie carried only an account and an expiry, so somebody already signed in stayed signed in for up to a week after the account was recovered. - **An empty instance can only have one first administrator.** Two registrations arriving together both counted zero users and both became admin. - **A long API key is no longer stored unprotected.** The check for "is this already encrypted" guessed from length and shape, and a long hex token satisfied the guess — so it was stored as typed and then read back as nothing, leaving the provider looking unconfigured while the key sat in the clear. - **One unreadable image no longer takes the service down.** A photograph the text recognition could not decode ended the whole server process; from outside it looked like a flaky gateway. It answers with an error now. - **The map library's critical vulnerability is closed** (MapLibre 6). #### Fixed - **Confirming flight detection no longer deletes your other trips.** It ended by removing every trip with no flight on it — which is every rail trip, every cruise, every hotel weekend — and their stops, routes, photographs and journal entries went with them. It ran even when the run created nothing at all, so confirming the dialog with nothing selected was enough. - **Merging two trips keeps everything on them.** Hotel stays and visited places lost the trip they belonged to; a linked photo album was deleted, and it took photographs the merge had just moved onto the surviving trip with it. - **"Tidy up" only offers trips that really are empty.** A trip carrying a hotel stay, a visited place, a linked album, a summary, a cover image or tags counted as empty and was offered for removal — pre-selected. - **A merged trip keeps its cover image.** The picture survived; the link to it still named the trip that had just been merged away, so the cover came back blank. - **Restoring a backup works, and says so honestly.** Restored uploads landed one directory too deep and could not be found under the names the database had kept; a restore over an existing database reported success while changing nothing, because the tool logged its errors and exited zero. The restore dialog also offered a field the server never read and no longer shows it, and it now fits on a small screen, announces itself to a screen reader, and closes on Escape. - **Deleting a hotel deletes its photographs.** The entries went; the image files stayed on disk for ever, inside every backup, with nothing left that could name them. - **A rejected upload does not stay on disk.** One bad request was enough to leave a file behind, quietly, every time. - **Clearing a hotel stay's dates clears what hung off them.** The dates went, but the check-in and check-out times stayed on a stay with no day to hang them on, the status came out wrong, and the exchange-rate day stayed on the old date. - **A stay's price counts the nights it actually had.** A stay recorded without dates but with a night count got no total at all, and one dated only by month was billed for the whole month. An explicitly free stay was billed at the per-night rate instead of staying free. - **A flight cannot arrive before it departs, and an ordinary westward flight is no longer refused.** Order was decided by comparing two wall-clock times read off two different clocks, so Munich 10:00 to London 09:45 — forty-five minutes in the air — was rejected, while London 10:00 to Berlin 10:30 was stored as a flight that lands half an hour before take-off. _This changes stored durations for flights that crossed a time zone._ - **A bulk import stores what a single entry stores.** Cabin class, aircraft registration, Mode-S address and every special-flight field were accepted and then dropped, so a flight imported as First Class carried a first-class carbon figure against a blank cabin. Imported prices also had no converted amount. - **Cost per hour and per kilometre ignore what they could not convert.** A booking in a currency with no available rate contributed its distance and its hours to the average while contributing nothing to the cost, so the rates read far cheaper than they were. _This raises the cost figures on the statistics page._ - **A trip without a flight follows its own dates.** A hand-made trip stayed "completed" after being moved into the future, through the edit, an explicit recalculation and the nightly pass alike. Trips built from hotel stays alone are now derived too. _This corrects the status of existing trips._ - **Trip detection keeps the flight you took, not the one that was cancelled.** A cancelled leg beat its rebooking purely by being first in the list, and two genuine departures from the same airport on one day were treated as a duplicate. - **A recorded track does not invent the stretch it did not record.** A GPX file marks where recording stopped and restarted; those gaps were joined into a straight line and counted as distance travelled. Adopting a track for a leg also re-measured the simplified line and came out short of the figure shown beside it. - **A change made while settings are saving is not lost.** On a slow connection the page marked the newer edit as saved and never sent it. - **Signing in as another account keeps that account's e-mail address.** The guard against leftovers from the previous account ran too late and deleted the address the server had just supplied — and the next save wrote the blank back. - **A bookmarked hotel, place, cruise or trip opens.** Opening a detail page directly bounced back to the dashboard while the settings were still loading. - **Settings survive a round trip.** The cost and tail-number switches came back to their defaults after saving, and cruise defaults reverted in a second browser, taking the good values with them on the next save. - **Seven flight fields are stored, and a delay follows its own schedule.** They were accepted and written nowhere; a corrected departure time left the old delay standing beside it. - **The profile form can be used with a screen reader**, and the account name is shown rather than offered for editing — saving it stored a second name that the next load replaced. - **Moving a stop moves the leg that ends at it.** The distance stayed as it was; a hand-drawn line now keeps its shape and is marked as needing re-anchoring instead of being silently trusted. - **The last English word in the German settings navigation is translated** (#321). #### Changed - **An instance's own settings are one record, and are read as one.** Nothing in the database said the settings row was unique, and eleven places created one if they found none — so two requests arriving together on a fresh instance could make two, after which a setting saved on one page was read from the other. - **The map keeps its colours.** The map rendering library stays on its current major version behind a compatibility bridge; the newer one draws every arc white. ## [2.6.3] - 2026-09-27 A fix for a sign-in failure on new installations, and the matching fix for installations behind an HTTPS reverse proxy. ### Fixed - **A fresh install reached over plain HTTP can sign in again.** Opening TravStats at a LAN address such as `http://192.168.0.10:3000`, the setup wizard and the login both answered "success", and the very next page said "No token provided" — the administrator could never get in. The bundled `docker-compose.prod.yml` passes `COOKIE_SECURE` through as an empty value when you set nothing, and the server read that empty value as "always mark the session cookie Secure". Browsers refuse to store a Secure cookie on a plain-http page, so the session was thrown away the moment it was issued. It went unnoticed because `localhost` counts as secure in every browser. An empty `COOKIE_SECURE` now means what the compose file always said it means: detect it — the cookie is marked Secure only when the request arrived over HTTPS, directly or through a reverse proxy that sends `X-Forwarded-Proto: https`. `COOKIE_SECURE=false` and `COOKIE_SECURE=true` behave as before. If you worked around this with `COOKIE_SECURE=false` and reach TravStats over HTTPS, remove that line again so the cookie is Secure. - **Behind an HTTPS reverse proxy the session cookie is marked Secure without configuration.** The web server inside the container replaced the proxy's `X-Forwarded-Proto: https` with its own `http`, so automatic detection could never see HTTPS. Until now that was hidden by the empty `COOKIE_SECURE` above, which forced Secure on everyone; with that fixed, the container now keeps the scheme your proxy reports, and falls back to its own when the proxy sends none. A proxy that does not send the header still needs `COOKIE_SECURE=true`. ## [2.6.2] - 2026-09-07 A round of tester reports from the days after 2.6.0 went out, plus the dependency and housekeeping work that had been waiting on the trunk since 2.6.1. Nothing here changes how anything is stored. ### Fixed - **A half-star rating can be saved.** The rating picker in a hotel stay offers half steps, but the server refused anything below a full star, so rating breakfast half a star failed the whole save with a red line at the bottom of the form. Half stars are accepted now, in the editor and in an import alike. - **The per-domain section of the statistics page speaks the reader's language.** "Distance", "Nights", "Places visited" and the rest were German words written into the code, so an English page showed a German block with German thousands separators. They are translated now, and the numbers follow the chosen language. - **"Next up" opens the thing it names.** Clicking the next hotel led to the hotel list, not the hotel; a flight belonging to a trip led to the trip. It now opens the entry's own page. - **A closed airport can be found by name.** Importing old bookings runs into airports that no longer exist, and a 2004 confirmation says "Berlin", never "TXL" — so Berlin-Tegel could not be attached at all. A search can now ask for closed airports, while an everyday search still leaves them out. The same search also honours the result limit it always advertised instead of always returning ten. - **A backup archive is compressed even behind a proxy.** An instance reached through a tunnel or reverse proxy served every response uncompressed, because the web server skips compression for proxied requests unless told otherwise. Nothing reported it; it simply cost bandwidth. ### Added - **Every time zone, not six.** Display settings offered Berlin, Paris, UTC, New York, Los Angeles and Singapore. The list now holds every zone the browser knows, grouped by region. - **A new hotel stay starts in the right currency.** The price field always opened in euros, whatever country the hotel stood in and whatever the account's own base currency said. It now follows the hotel's country, and falls back to the account's currency. A saved stay keeps what it was saved with. ### Changed - **Two-factor secrets survive a library upgrade.** The one-time-password library moved three major versions; the new one refuses the shorter secrets the old one issued, which would have locked every account that had two-factor switched on. The shorter secrets are still accepted. - **A flight lookup that answers unexpectedly is named, not mistaken for a crash.** One flight-data provider occasionally answers with something other than a list; that used to be logged as a failure and cached. - **The image states its licence correctly** (AGPL-3.0-or-later), and the route-distance backfill runs inside the container as documented. - Dependency updates across the frontend and backend, including the translation runtime, and the rate limiter's handling of IPv6 addresses so one host can no longer take a fresh bucket per request. ## [2.6.1] - 2026-09-05 Two things the owner ran into on the day 2.6.0 went live, both on the edit side: a flight recorded without its times could never be given them, and a house with many stays turned its page into a scroll marathon. ### Fixed - **A historical flight can leave "historical" and be given its times.** The edit form offered no way to enter a clock on such a flight: its status is a read-only pill, and the year/month/day pickers carry no time — so a flight imported without its times was stuck that way for good. The edit form now has the same "historical flight (route only)" checkbox as the create form. Unchecking it shows date and time; a known day is kept, a year-only or year-and-month date becomes empty rather than a fabricated January 1st, and the save is refused until both clocks are typed, so no midday is invented on the way out. The status is then derived from the times, as everywhere else. Checking the box on a flown flight is the reverse move. ### Changed - **The stays of a house scroll in a box of their own.** A house with dozens of stays stretched the whole detail page, pushing the map, the spend card and the ratings off screen. The list now scrolls inside a bounded box beside them, on wide screens only — on a phone the page keeps scrolling as one, so there is no scroll trap inside a scroll. ## [2.6.0] - 2026-08-19 The release where the other ways of travelling catch up with flying. Hotels become a domain of their own — recorded, imported, mapped and counted like flights and cruises — and every price can be written in the currency it was actually paid in. Cruises, stays and places gain statistics at the depth flights already had, and a trip can now be walked, driven and cycled rather than only flown. Two-factor authentication and passkeys arrive alongside. Some of the larger additions ship behind the instance beta switch, which is off by default: the passport, the places domain and per-domain colours. They are complete and tested, but had not been through a release review in time for this one. Tours, device pairing and the Dawarich connection started out behind that switch too and came out from behind it in the later candidates, once the conditions their own gates named had come true. The admin panel lists exactly what the switch turns on, read from the same registry the application itself checks. The candidates after rc.27 also rework how countries are counted — by the evidence that proves a visit, not by where a flight touched down — and carry the findings of a full audit of the trackers on 4 September: twelve defects a tester found on the release candidate, three map defects, and nine older fixes that had shipped without a test. ### Added - **A country counts by what proves you were there, not by where a flight touched down.** The country count was too high and too low at once, which is why it looked plausible for so long: a four-hour connection counted as a visit, while a country reached by car and slept in for a week did not exist at all, and three countries with completed hotel stays and no flight ever were missing. Every country now carries the strongest evidence for it, in four rungs — _slept_ (a completed stay, a house on record, a port call over a night, arrival and departure on different local days), _visited_ (a same-day stay or a recorded place), _transited_ (driven through, read from location history) and _connection_ (changed planes and nothing else). Only the connection rung is left out of the headline by default; a connection country is greyed and kept, never hidden, because the tier is inferred from recorded flights and flights are recorded incompletely. A day trip is no longer a connection: MUC–FCO–FRA is a day in Rome. Cruise port calls and recorded places now prove a country the way a landing does, and a house entered without any stay counts as one night (owner's rule); a booking still ahead and a cancelled stay prove nothing. There is deliberately no hour threshold — six hours and twelve hours returned the same set of countries on a real account, so a dial would promise a precision the data does not hold. Only attested days count: a landing in January and the next departure five years later no longer read as five years of presence. **This moves the headline for every account**: it rises for accounts with cruises, places or hotel stays, and falls for accounts whose countries rested on connections. - **Every counted country names its evidence, and the record is one click away.** The passport table shows each country's rung, the records that proved it, the days present, and the time on the ground where a pair of flights bounded one. A duration that cannot be derived reads "unknown", never zero, and a country no flight touched shows a dash — the two differ because only the first says "add the missing flight". Expanding a row lists the flights, cruises, places and houses behind it, each linking to the page that edits it. A country page (`GET /stats/countries/:code`) answers for a country reached only by ship or by a recorded place, instead of a 404 behind a clickable row. - **How strictly countries are counted is a setting — an instance default, and a per-user override.** Whether a connection counts is a personal definition, not a property of the server, so an administrator sets the starting point and anyone may override it. The choice is one of the evidence rungs; there is no hours option on purpose. Changing it moves the headline and nothing else: the country list is identical at every threshold, and the year in review and the achievements read the same threshold rather than counting airside stopovers. Badge progress can drop for an account holding connection-only countries; a badge already earned stays earned. - **Driving through a country is its own rung, read from location history.** A Dawarich connection is now reduced nightly to country-days — "on this day, in this country" — and folded into the passport as a fifth kind of evidence. Coordinates exist only for the length of one loop and are never stored, returned or logged; what a row keeps is the day, the country and a count of fixes, so a day held up by four hundred points and one held up by a single point can be told apart. A country is a connection when every point ever recorded there lay within an airport's grounds you are known to have flown through; a point beyond that lifts it onto the stronger rung. The road rung and its filter only appear once an account has location history, because a filter that always returns nothing reads as a bug. - **The passport says once that the counting changed, with your real figures.** A number that moves without explanation reads as data loss, so the page shows "Zählweise geändert — vorher 32 Länder, jetzt 35" the first time the old rule and the new one disagree, computed from the same rows so the two can never drift, with a link to the evidence explanation and a dismiss that is remembered in your browser. - **Data that contradicts itself is flagged and queued — never rejected, never silently corrected.** Four checks fire only where two sources inside one record disagree: an address whose country differs from the stored one, coordinates that fall outside the claimed country, a country resting solely on records with no date, and a check-out before its check-in. A third-party geocoder does not get a veto over your own data, so the record is written and the question waits. A flag never changes a number by itself. A point in the sea or in a territory the offline outlines do not attribute is not a disagreement. A nightly sweep and every import run the checks. - **The pending-updates page is now the inbox.** It holds the flight proposals it always had plus the data-quality questions, one list for the user with two tables underneath. Each flag states both conflicting values and marks neither as correct. The two answers are different kinds of statement, and each carries its consequence: "I corrected it" lets a surviving contradiction come back; "This is right" never asks again — the escape hatch for a district that shares a country's name. The nav badge counts both halves independently, so one failing cannot hide the other. - **A document announces itself.** A pasted mail, a `.msg`/`.eml` file or a PDF can be sent with `domain: "auto"` and the server decides whether it is a flight, a cruise or a hotel — a property of the document, not of the button that sent it. Detection is a weighted scorer rather than the language model, so it works on an instance with no Ollama; a signal counts once however often it repeats, because marketing mail is the longest thing anybody forwards. A weak answer still returns what it parsed, with the runners-up, so a client can offer a switch instead of asking for the file again. An explicitly named domain keeps its exact previous answer (forgejo#57). - **A photograph is a document.** `POST /parse-image` reads a photographed confirmation — a hotel bill, the most photographed travel document there is — through OCR and hands the text to the same parser as a mail or a PDF. Until now an image could only ever become a flight. The OCR worker reads German as well as English, since the lodging parser matches on "Nächte", and degrades to English on an air-gapped instance rather than failing (forgejo#58). - **A photographed boarding pass is read by its barcode first.** The parse route decoded only the printed text while the propose route beside it, handed the same picture, decoded the PDF417 or Aztec code and used OCR merely to fill gaps — so a Wallet screenshot could come back "unreadable" with the answer sitting in the middle of the picture. A decoded barcode wins every field it carries; OCR keeps the fields no barcode holds (gate, terminal, boarding group). The airline is now reported by its printed name rather than its two-letter code on both routes (forgejo#37). - **The travel records, the whole route network and the year in review come from the server.** `GET /stats/records` computes the seven records (longest and shortest leg, longest time aloft, streaks, delays) once, as numbers with units rather than formatted sentences; `GET /stats/network` returns every airport with coordinates and every route with a count and a distance, deliberately unbounded because a truncated network is not a smaller globe but a wrong one; `GET /stats/wrapped?year=` reads the year off the data and reports a favourite the year cannot support as absent rather than zero, and a year with no flights no longer headlines as the biggest flying year of your life (forgejo#41, forgejo#42, forgejo#44). - **Four more curated checklists, each verified rather than recalled.** The New 7 Wonders of Nature, the UNESCO Biosphere Reserves (646 of ~759 — the register entries with no coordinate are dropped rather than pinned at 0/0, and the gap is stated), and the national parks of Germany (16) and the United States (63). Two coordinates guessed from memory turned out to be an Italian village and an American singer, so every row was checked; a natural wonder gets an arrival point, never the centroid of a rainforest (forgejo#66). Alongside them, fourteen museum warships — USS Missouri, HMS Victory, Mikasa, Vasa, U 995 — with two collector achievements. - **A geotagged photo suggests the place it was taken at.** A trip with a linked Immich album is already a list of dated, positioned points, and nothing was reading them. A photo is the only anchor that is not a proxy — a hotel is booked in the town you came to see, but a photograph is a GPS fix at the moment the shutter opened — so its radius is one kilometre and it outranks a hotel or a logged place as the stated reason. - **Places can be imported from a CSV or a Google Takeout list, and the rows it cannot place come back to you.** German decimals are read as such ("41,8902"), and a row with no coordinates — every row of a Takeout export — is neither dropped nor written: it returns as _needs input_ with your note intact, for one click in the picker. A place added by hand now carries the same identity a later import would mint, so the Colosseum picked today and imported next month is one place, not two. The batch appears in the import log and can be undone. The web import panel for places stays behind the beta switch with the rest of the domain; the routes are live for the API and the Companion. - **Several flight proposals can be answered in one request.** `POST /pending-updates/apply` and `/reject` take a list of ids and report an outcome per id — a flight edited meanwhile, one already applied, one that is not yours — rather than one success flag that would silently drop changes you believe you accepted (forgejo#33). - **The backlog refresh says which kind of nothing happened.** "The provider has nothing on this leg" and "the provider answered and your record was already complete" shared one counter, so a run that had just written three tail numbers read as a run that changed nothing. _Already complete_ is its own line now, and every silent per-flight result carries the provider's own reason — no key, a date the free tier refuses, or a leg it genuinely does not know. - **A flight that landed without its actual times is asked about one last time.** For an overnight flight both arrival checks fell on a date the free plan refuses, and once the sweep marked it flown nothing ever asked again. The sweep now leaves one late check outstanding and spends it through the one provider that answers for a past date — exactly once, values filled and never overwritten, and only for flights the sweep has just flipped, so the cost is the number of flights you actually took, not the size of your logbook. - **A fresh install lands on a starting point, not an empty dashboard.** The empty state says what the map will show and routes to the import section (#237). - **An account carries a first and last name**, shown in the account menu and returned with the profile (#241). - **Tours: a trip can be walked, driven and cycled, not only flown.** A trip now carries route sections of its own — road, hike, bike and ferry — each with a section list, an editor that works with the mouse, and its own line on the dashboard map under a Touren tab. Recorded tracks can be pulled from a Dawarich instance and adopted onto a section. Hidden behind the beta switch: everything works end to end, but the feature has not been through a release review, and the Dawarich connection carries a second switch of its own because a connection with nothing to connect to is worse than no card at all. - **A passport.** Stamps, a continent quota band and a country table, built entirely from flights already recorded — nothing new is asked of you. Behind the beta switch for this release. - **Statistics for cruises, stays and places, at the depth flights already had.** Each gets its own rhythm, quality and curiosity blocks rather than a handful of totals. Cruise money is deliberately reported per currency and never summed: cruises carry no exchange-rate snapshot, and adding the numbers would have repeated a bug that already exists one domain over. - **Photographs belong to the hotel.** A picture of the building, the lobby or the view is a fact about the house and survives every stay, so it hangs off the lodging rather than off one visit. - **A statistics tab shows only the blocks you want.** Someone who records no prices had a cost block on every screen. Only the HIDDEN blocks are remembered, so a section added in a later release still appears rather than being silently swallowed by an old preference. - **Twenty-five new achievements for places**, taking the catalogue to 276. Places were the thinnest domain; cities, continents, streaks, latitudes and ratings now count for something. - **The beta switch says what it turns on.** The admin panel lists every gated feature with a plain sentence, read from the same registry the gates themselves use — so it cannot claim something the build does not do. - **A per-domain colour can be chosen** rather than taken from the brand. It applies to statistics, the trip timeline, the activity rail and the import log. Behind the beta switch while the question of whether an instance may repaint its own domains is settled. - **The route editor is worked with the mouse.** Clicking a different leg switches to it, which it did not before: the click was swallowed and quietly added a waypoint to the leg already open, so reaching for one leg edited another. Right-clicking now offers what fits under the cursor — edit that leg, delete this waypoint, delete the whole selection, simplify, close. A Select tool turns the drag into a selection box, so several waypoints can be removed at once, and the ports are never among them. One undo takes a box-delete back whole. - **The API documentation is complete.** It described a curated subset while claiming it could not drift; measured against what the server actually serves, 244 endpoints had no entry. All of them are documented now — 241 operations covering every domain a normal account can reach — and the check that found the gap has become a plain completeness test with no exception list left to add to. An endpoint written from here on fails the build the day it is written unless it is documented with it. - **A cruise or a flight on a trip's timeline opens where it stands.** They could not be clicked at all — not viewed, not edited — while the hotel entry beside them already led to its own page. A click now expands the entry inside the trip: the cruise shows its ship, cabin, price and the itinerary day by day, with sea days named as such; the flight shows airline and number, aircraft, seat and class. A button in the panel opens the full page for editing. - **A list's symbol is picked, not typed.** The symbol field was a bare text box: you had to know an emoji and then find one to paste. There is a picker behind it now, with search — typing "pommes" finds the fries. Its emoji database is served by your own instance rather than fetched from a content network, so it works on an installation with no way out to the internet, which is most of them. Pasting into the field still works, and if the picker cannot load it says so instead of leaving a control that does nothing. - **A places list can wear its own symbol on the map.** A list of fifty-eight McDonald's spelled the same name fifty-eight times across the map, which reads as noise rather than as a list. A list can now carry a symbol and ask for its places to be labelled with that instead of their names. The dot underneath is unchanged: it still carries the list's colour, and still tells a visited place from one on the wishlist by being filled rather than hollow. A list without a symbol keeps its names, and so does a place that belongs to no list. Above all of it the map has one switch of its own — as each list says, always names, or always symbols — so a single flip brings every name back for a moment without editing a list. - **The places list can be filtered by list.** It could already be narrowed by status, category and country, but not by the lists you build yourself — even though the dashboard had offered exactly that all along. With 58 McDonald's in one list sitting beside a hundred other places, "show me only that list" is the filter that makes the page usable. A list with no entries yet shows nothing, rather than quietly showing everything. - **Flights are part of the spreadsheet, in both directions.** The export carried cruises, lodging and places but not the largest domain; now it does, with the eighteen fields you would actually edit in a table. Airports travel as IATA codes rather than coordinates — a code is what you can type and check — and the server resolves one into a real airport, which is what makes adding a missing flight from a row possible at all. Changing a code moves the flight; an unknown code is refused rather than quietly leaving the old route. - **Visits can be imported.** The export always wrote a Visits sheet and the import ignored it without a word, so editing visits and sending the file back did nothing. A visit now imports against its place — several visits to the same place on the same day stay several visits. Cruise stops and stays remain export-only, and the import panel says so instead of leaving you to find out. - **The spreadsheet import asks what to do with what is already there.** _Add only_ creates and never touches an existing entry. _Merge_ — the default — updates the rows the file names and creates the rest. _Replace contents_ additionally deletes everything the file leaves out, and is the only mode that can lose data: it takes a full backup before writing a single row, shows how many entries would disappear, and will not proceed unless you confirm that number. If the backup cannot be made, the import does not run. An empty sheet deletes nothing at all — clearing a domain has to be asked for row by row, never by handing over a blank tab. - **The whole logbook exports to one spreadsheet, and comes back.** A sheet per table — cruises and their stops, properties and their stays, places and their visits — readable, filterable, and re-importable. Column A carries the row's id: a row that returns with one updates the record it names, a row without one is created. An import is previewed before anything is written, and shows what it would change per sheet. A row carrying an id that is not yours is refused, never applied. - **A place gets its address from its pin.** Places arrived located but undescribed — the checklist catalogue has no address column and names a country for 14 of its 1,261 entries. New places now fill in address, city and country when they are saved or ticked, and a nightly pass fills in the ones recorded earlier. - **Places can be sized on the map, like every other domain.** The map settings had a size slider for flights, cruises and lodging but not for places; the place dot was pinned at one size and its labels at one zoom level, so zooming in never revealed another name. - **The API documentation covers cruises, catalogues, geography and achievements.** It described flights and trips and claimed to be complete; measured against what the server actually serves, 244 endpoints had no entry. Coverage is now enforced by a test that walks the live route table, so an endpoint can no longer ship undocumented. The remaining gaps are listed by name rather than left to be discovered. - **Every dashboard tab has the same activity list.** Four tabs carried four different panels, places carried none, and "All" listed flights and cruises while the tab bar above it counted four domains. One list serves all of them now, and picking an entry focuses it on the map instead of navigating away from the dashboard — a hotel used to throw you off the dashboard entirely. - **The lodging list has a "Last stay" column, and it sorts.** A hotel is dated by its newest stay, planned ones included, rather than by the order the hotels were typed in. - **A list remembers how it was sorted.** The choice survives a reload instead of springing back to the name column. - **A place can be attached to a trip.** The visit form offers the trip, the way a lodging stay always has. The interface never set it, though the API had accepted it from the day the visit routes were written. - **A cruise can be attached to a trip.** The cruise form offers the trip, the way a flight, a lodging stay and a place visit already did — a cruise was the last entry whose trip could only be set through the API. The list and the cruise page name the trip it belongs to, and the trip counts the cruise and its price. - **A place list you build yourself, and checklists we ship.** Group places into named lists — a wishlist, a route, "the ones worth the detour" — and reorder them by hand. Alongside them come curated checklists: the New 7 Wonders, the Seven Wonders of the Ancient World, and the complete UNESCO World Heritage List with 1,247 sites. Subscribing writes one row, not 1,247: a catalogue target becomes a real place only when you tick it, so a corrected coordinate still reaches someone who subscribed a release ago. Ticking never happens by itself — the suggestion logic proposes and you confirm, because a wrongly set tick is indistinguishable from a real one afterwards. - **Proof for a visit, with a caption.** Photographs attach to a single visit and carry a caption you can type. - **Places have a place in the map settings**, and on the map they are drawn as the same plain dot every other domain uses — including in the key beside it. - **Places become a domain of their own — hidden behind the beta switch.** A place is a first-class entry with its own list, detail page, form and map layer; a trip is a _view_ of it rather than its owner, so "every McDonald's I have been to" is expressible and the same place on two trips is one entry with two visits. A place is visited, planned or on the wishlist — planned is derived from a dated future visit, never stored. Invisible to ordinary users: the domain, its routes and its dashboard tab all sit behind the instance beta flag, and turning the flag off hides the data without touching it. Two migrations ship with it; the second copies coordinate-carrying POI trip stops into places and deletes nothing. - **A flight has a page to read it on.** A flight carries around 58 fields and the table shows nine columns; everything else — seat, gate, terminal, booking reference, companions, baggage allowance, overflown countries — was readable only by opening the edit form, which meant putting the record into an editable state to look something up. The row now opens `/flights/:id` the way a cruise row and a lodging row always have. - **Every list summarises the rows it is showing** — count, and the figures that matter for that domain — marked "filtered" when a filter is narrowing them, and silent while nothing is known. - **The sea route of a cruise can be corrected by hand.** Enter edit mode on the detail map, tap a leg, and drag its waypoints — insert new ones on the guide line, remove them on hover, undo and redo the whole gesture chain. The corrected line is stored with the cruise, its distance replaces the routed one, and "back to automatic" returns the leg to the router at any time. A leg arriving with dozens of raw router points presents at most 25 handles — the ones that carry the shape. - **Pick a point on the map, everywhere.** Every address input — lodgings, trip stops, custom ports, special-flight venues — now opens one large map modal: search, click, or drag the pin, and the resolved address underneath is filled into the form on apply. It replaces the cramped inline mini-map. - **The booking total is read from the mail.** The LLM parser now extracts the final price and its currency from a booking confirmation; the total lands on the booking, not on each flight segment — "Endpreis EUR 4,359.14" no longer arrives as an empty price. - **Trips from imports are optional — and dated.** A new switch on the import hub controls whether flights sharing a booking reference silently become a trip; switched off, the booking reference is kept so "detect trips" can group them later. Auto-created trips now carry the date range of their flights instead of none. - **Hotels and other stays are a full domain.** A lodging carries its type (hotel, guesthouse, apartment, hostel, campsite), its chain, its position on the map and its own stays; a stay carries dates, room, board, per-category ratings, price, loyalty programme and the trip it belongs to. The domain switches on and off like the others — off means it is gone from the navigation, the dashboard, the map, the statistics and the achievements, while the data stays untouched. - **Booking confirmations are read for you.** Booking.com mails are parsed by a deterministic template — measured against 95 real confirmations, it now finds the stay in 93 of them and the price in all 93, across both layouts the sender uses. Anything the template does not recognise falls back to the LLM path, exactly as flights and cruises do, and PDF attachments go through the same pipeline. - **A CSV import that shows you what it will do first.** The preview names every row it considers questionable, orders those first, offers a mapping for the columns it could not place, and commits the run as ONE batch that can be reverted whole. Hotels without coordinates are geocoded afterwards, in the background, so a slow lookup never blocks the import. - **Loyalty programmes live in one place.** A card is created once, covers as many chains and individual hotels as it really covers, and each stay derives its programme instead of asking again. A programme that is already on file offers to extend to the chain you are looking at rather than refusing you. - **Any currency, and an honest total.** A price can be recorded in any of the 155 ISO-4217 currencies. Conversion asks the European Central Bank first — authoritative and self-hostable, back to 1999 — and, for the currencies the ECB does not publish, a keyless public dataset that an administrator can switch off entirely. Where no source has a rate, the amount is kept in its own currency, marked "kein Kurs", left out of every total, and the total says how many stays it left out. You may enter a rate yourself; it is always shown as yours, never as an official one. - **Two-factor authentication and passkeys.** TOTP as a second factor with single-use recovery codes, or a passkey that replaces the password entirely. Both are configured in the new security section; an administrator can reset a locked-out user's second factor. - **Statistics and achievements grew with the domain.** 41 new lodging and cross-domain achievements, punctuality figures for flights (average delay, on-time rate, best and worst airline and route), trip-level insights, and a spend breakdown per currency. - **Places are picked, not typed.** One shared location input — search, pasted coordinates or a pin on the map — now serves hotels, cruise stops and custom ports. The geocoding services are instance-configurable. - **Two doors for getting data in.** A one-off list import lives in the settings; every domain page and the dashboard offer the single-entry chooser, where the first route IS the drop zone — one control takes a `.msg`, an `.eml` and a `.pdf` alike, so nobody has to classify their own file first. - **Every import leaves a trace, and can be taken back.** Flights, cruises and stays record where each row came from and which run created it, so the same document read twice adds nothing the second time, and a whole run can be undone as one batch. - **A Google Maps saved-places list, imported as itself.** Each row carries Google's own id for the place, so a second import recognises what is already there. Such a list holds no dates and no prices, and the import says so. - **Package-tour travel documents are read.** Measured against seven real bookings: the invoice yields the trip, its price and its flights with IATA codes; the travel documents yield the day-by-day itinerary and the hotels with their addresses. Fourteen lines that look like flights and are not — un-ticketed placeholder legs, separately ticketed ones, the rail transfer to the airport — are refused WITH a reason rather than dropped silently. - **The trip map shows the hotels**, not only the flights: one pin per house with this trip's nights on it, and the flight arcs lie flat on the map instead of bowing out of it. - **The lodging table says what a row still needs** — an address the geocoder could not find, no address at all, a name and nothing else, or a country field holding something that is not a country. Nothing is shown for a sound row. - **The logostream and Google Places keys can be tested from the admin page**, like every other provider key. The Places check sends one real, billed request and its result names the price (~0.03 USD) — a test that costs money must say so. - **Hotel statistics grow into their own chapter.** What a night cost — by year, country, chain and board, with the median taken over nights rather than bookings — and what it was worth: the value of award nights, and all four rating columns separately instead of one blended score. Where the nights happened: continents, the northernmost night, the centre of gravity of your travel computed on the sphere. When they happened: by weekday, by season, and the longest unbroken run away from home. Nights per loyalty programme per calendar year. And a travel account that splits every year into hotel, ship, aircraft and home — naming the days it cannot account for rather than hiding them. - **34 new achievements** for the measures that had none, in German and English. - **A hotel you cannot date is still a hotel you slept in.** A stay may now record only a month, only a year, or no date at all, and says which — instead of forcing a guess that later reads as fact. - **A stay can carry its check-in and check-out time.** Both are optional and refine the day rather than replace it — night counts, statuses and currency conversion keep reading the dates. The "Next up" line counts down to the real 15:00 check-in instead of midnight, and no longer drops today's stay the moment the date turns. - **The map modal answers "what is here?".** Dropping a pin now lists the named places around it — the hotel, the snack bar, the station. Tapping one snaps the pin there and carries that place's full details into the form, while confirming without picking keeps the plain reverse-geocoded address as before. Address and place lookups run in parallel, so one failing never takes the other down. - **19 more achievements**, this time for the curious corners: Friday the 13th, a Christmas flight, a palindrome date, flight number 666, a 777 flown on a 777, three flights in one day, a same-day out-and-back, a single flight across eight time zones, the aisle-seat streak that completes the window/middle trilogy, a flight below the Antarctic Circle, an equator crossing at sea, same-ship loyalty, and the diamond rungs of the airline and aircraft ladders. - **Country flags from the server.** `GET /api/v1/country-flags/:iso` answers with a flag SVG (`variant=flat` or `square`), cached privately for a day with an ETag, and a 404 is a valid answer; `GET /api/v1/country-flags?codes=DE,FR` returns up to 250 flags in one request, so a client can preload the countries it actually has. Flags and airline logos now come from one source. - **Days away, per domain and in total.** `GET /api/v1/stats/summary` carries `daysAway`: the distinct calendar days on which the account flew, sailed, slept or recorded a place, and their union. Days, not kilometres or nights, are the one measure the domains can be compared on, and the union is what a client cannot compute itself without counting a hotel night during a flight three times. - **Passport rows say what was recorded there.** Each country on `GET /api/v1/stats/passport` names its port calls and recorded places, and a country proved by a stay carries the city slept in most and the nights. - **A trip lists its route count.** `GET /api/v1/trips` carries `_count.routes` beside flights, cruises and stays, so a trip row can read "6 entries · 3 sections" without one request per trip. ### Changed - **Tours, the Companion app pairing and the Dawarich connection stay behind the beta switch.** All three work end to end and were briefly offered to everyone in a release candidate; the owner ruled them unfinished. An instance with the switch off does not offer the Tours tab, the trip route editor, the Devices section that mints a claim code, or the Dawarich card. Each gate names why it is still there and what brings it back; the places domain stays gated as well (forgejo#65, forgejo#73). - **A route is the pair, not the direction.** FRA→WAW and WAW→FRA were two routes with one flight each while the Companion's globe counted one route with two. Both legs now land on one entry, keyed by the two codes sorted, so an existing account's top-routes list reorders: two entries of one collapse into one of two (forgejo#42). - **A flight's duration is owned by the database and cannot go stale.** It was derived independently in three server places and again in the app, and eight code paths write the timestamps it depends on. It is now a generated column that Postgres refuses to hand-write, so no present or future write path can leave an old number behind. Rows whose duration depends on the airport catalogue are deliberately not stored, so a timezone correction lands on the next read. On the way, `GET /flights/:id` and the create and update responses stopped returning a bare row that could show a null duration where the list showed a number (forgejo#45). - **The flight table is indexed for the shape the statistics actually ask.** Two composite indexes replace ten single-column ones for the hot queries; on a multi-user instance the counting queries stop reading other users' rows (measured 5007 buffers down to about 20). On a single-account instance this buys nothing and costs nothing — the planner ignores an index that does not help (forgejo#48). - **"Transit" is renamed "connection" wherever a country's evidence is shown**, in stored settings as well as on screen, because _Transit_ is the word a German reader attaches to driving through the moment a road rung exists. A stored threshold of "visited" becomes "transited" by the same reasoning, so no instance's headline moves on the rename alone. - **Newest first, everywhere.** What is coming up stands above what is past, and an entry with no date sits at the bottom rather than sorting as though it happened in 1970. - **The cruise route editor opens full-screen.** "Edit route" used to arm an editor inside the small preview card in the page margin, where a leg is a hairline: hitting one took three attempts, so the button read as one that does nothing. Same editor, same handles, on the whole window. - **One currency, one distance unit.** There were two currency settings: one under Einheiten & Formate that only flight surfaces read, and one under Unterkunfts-Präferenzen that lodging, statistics and achievements read — the general one was domain-specific and the domain-specific one was general. There is one now, under Einheiten & Formate, and it applies everywhere. Conversions already recorded keep the rate they were taken at. The distance unit now also governs cruise distances, which were fixed to kilometres while flight distances followed the setting. - **Funktionen and Automatische Updates moved to the Flug tab**, where their only consumers are: both Funktionen toggles are read by flight surfaces alone, and auto-update runs entirely against flights. Existing links to those sections keep working. - **The three domain lists behave alike.** The same actions as icons, the same message when loading fails, a clickable row on every list, and one filter bar — search, status and year open everywhere, everything a single domain owns behind one button carrying a count. - **Six delete dialogs said six different things.** One shape now: what is deleted, how much goes with it, and what survives. Deleting a trip says for the first time that its photos go with it. - **The three domain lists are one family.** Flights, cruises and lodging share the same table now: status pills, sortable column headers, a column picker, and more width. - **The next upcoming entry moved into the domain strip.** It used to float over the map and covered the map-mode switcher completely. It now sits beside the tabs, follows the active tab, and names the trip it belongs to. - **The map legend names what it draws.** Lodgings appear as a circle rather than a line — a stay is a place, not a route — and airports and ports finally have a swatch of their own. - **Every `/api` response is `Cache-Control: no-store` by default.** A shared cache was observed serving one signed-in user's response to another. - **The import hub carries every enabled domain.** The e-mail and PDF routes moved back out of it on purpose (2026-08-15): one door for lists, one for a single entry, in each area's own add dialog. - **A stay counts once it is over.** Lodging figures counted every house on record, including ones only bookmarked from a saved-places import, and bookings that have not happened yet. A bookmark is not a visit, and a booking for next month is not a night slept: both are now reported separately rather than folded into a total or dropped from it. What counts is decided in one place. - **Lodging pins take a colour mode** like flights and cruises, the list panel remembers whether it was open, and the figures moved off the map into the statistics, where figures belong. ### Fixed - **"What's new" tells the whole release.** The 2.6.0 overview listed five points and left out the ones a reader would notice first — countries counted by evidence, tours and the Companion pairing out of beta, the spreadsheet round trip, the grown statistics — and said nothing about the beta switch. Eight points now, and the one that describes what sits behind the switch wears a Beta mark, so nobody looks for a page their instance does not show. ### Fixed - **A booking that names its airport is filed into that airport, not into the city's current one.** A 2008 Germanwings confirmation printing "Berlin-Schönefeld" came back from the parser as `BER`, the code of an airport that opened in 2020, and two flights were filed there. When the mail carries the distinctive name of another airport of the same city, closed ones included, that airport's code replaces the model's; two names on one leg, or the returned airport's own name, leave the code alone. - **Two beta gates closed properly.** With the instance's beta switch off, the statistics page still offered the "POI / Besuche" tab, chip and summary card to an account that had the domain on from beta days, and the passport URL answered with "enable the flights domain" to a reader whose flights were on. The tab strip and the overview follow the same rule as the deep link now, and a closed passport gate goes home like /places. - **The inbox is always in the menu.** The Posteingang entry under System appeared only while something was open, or while you were already on the page — an empty inbox had no way in from the UI at all. The entry is always there now; the badge alone says whether anything waits. - **A flight saved with a date and no times no longer counts its placeholder clocks as flight time.** Such a row carries "12:00 → 13:00" that nobody measured, and three figures subtracted it anyway — the business tile's cost per flight hour, the achievement engine's hours in the air and the summary a pending update compares against — while the overview beside them estimated the same flight from its distance. One rule now, shared by every screen: the clocks count only when they are real instants; otherwise the flight is estimated from its coordinates and labelled as such, or contributes nothing at all. Hours in the air and cost per hour move for accounts with such rows. - **The direction arrows are back on the map.** rc.28 was the first candidate to carry the page's Content-Security-Policy, and its `connect-src` did not admit `data:` — deck.gl fetches its icon atlases from inline SVGs, so every cruise arrow and the aeroplane marker vanished with "deck: Failed to fetch" in the console. `data:` and `blob:` are allowed for connections now, and a test reads the nginx config so the scheme cannot be tightened away again. - **Refreshing the backlog keeps the aircraft type and the actual times.** Three flights the owner had just taken came back with a registration and nothing else, out of a provider response that carried the aircraft model beside the registration and both actual times beside the scheduled ones — five fields were copied out and the rest dropped. The patch now carries all eight, on the same never-overwrite terms; the candidate query was widened so flights refreshed once are not locked out for ever, and it asks for both spellings of empty (125 rows NULL against 35 rows `""` on a real account — all three reported flights were in the 35). - **An overnight flight no longer gets the neighbouring day's aeroplane.** The provider answers with every flight that touches a local date — departing on it or landing on it — and for an overnight flight those are two different aeroplanes a day apart. LX93 stored as departing on the 2nd was answered with the flight that landed on the 2nd, which the rotation guard rightly refused, so every long-haul leg on the account carried no actual times while the short hop beside it had its own. Candidates are filtered on their local departure date before the pick. - **A flight in the air is asked about, whatever day it left on.** An up-front gate refused any live lookup for a flight whose departure date was not today — on the reasoning that real-time queries only cover today. An aeroplane that took off yesterday and has not landed is on the real-time feed this minute; both arrival checks of an overnight flight were being refused. The exception asks for a measured position in the air, so an ad-hoc lookup for a date last May is still refused as the historical question it is. - **A flight number is not unique within a day — the airport decides.** LX93 on 2026-09-02 came back three times: the long-haul that left the day before, a feeder that afternoon, and the one actually flown. The departure airport now narrows the candidates, as a preference rather than a filter, so an older row with no code is no worse off. The lookup cache key carries the airport too, because two flights sharing a number on one day are different answers and the old key would have served the first caller's aeroplane to the second. - **A provider may not rename the carrier you flew with.** LX1104 Zurich–Munich, operated by Helvetic and stored as such, was rewritten to "United Airlines" by an auto-applied update while the codes beside it still said Helvetic — on a codeshare the marketing and the operating carrier are two true answers and the provider may report either. Your own text now wins; an empty column is still filled. It also ends the "SWISS" against "Swiss" proposal on every refresh. - **The lodging import finds a house under a decorated name.** "Hotel Meteora" beside "Hotel Restaurant Meteora", "Krafft Basel" beside "Hotel Krafft Basel" — five pairs on one account, because the matcher keyed on the exact name and the coordinate rule behind it never fired: a booking mail and a saved-places export both arrive without a pin. Two names are now offered as one house when the town agrees and they share an identifying word; a hit is a guess you confirm, never a silent merge, and "Hotel Rose" in Portland and in Bietigheim stay two houses (forgejo#84). - **The postcode stays out of the city, from both parsers.** "188973 Singapur" and "BW 78467 Konstanz" sat in the city column: the template admitted four or five digits and nothing in front, the language model copied the city as printed, and the geocoder never repairs a city that is wrong rather than empty. One splitter now takes a leading code off the city — six digits, a state abbreviation in front, Dutch letters, a Luxembourg L- prefix — and both parsers use it (forgejo#85). - **The same airline is the same code, on every surface.** One account counted 31, 27, 26 and 25 airlines on four screens, because identity was the name: "SWISS" and "Swiss" were two carriers, "LOT" and "LOT - Polish Airlines" two more, and four flights with no airline formed a group labelled "". Two rows are now the same airline when they share an IATA code, resolved through the catalogue; a row with no airline is counted apart and the breakdown says how many it left out (forgejo#81). - **"Länder besucht" for a single year no longer shows the lifetime set.** The lodging domain had no per-year country index, so the overview fell back to every country ever slept in under a "2024" header — 35 where 15 were flown and 7 slept in that year. Stays are keyed by their check-in year now, like flights. A house entered without any stay still counts as its country; that is the owner's rule from the country-counting design, and the issue's second half stands the other way (forgejo#80). - **A planned, priced stay no longer reports its hotel as "0 €".** The spend cell chose its branch from all stays but summed only the ones that count, so a hotel not yet slept in read as free. Nothing counted reads "—", and a priced stay still ahead shows a muted "geplant: 149,90 €" beneath it (forgejo#82). - **"Add POI" on the map opens the place form.** The map's "+" menu offered it whenever the domain was switched on, and clicking it did nothing — the slot held a comment from before the domain existed. The form now opens and stays on the map after a save, so you see the pin; the menu entry follows the same visibility rule as the tab strip (#288). - **One formatter for every amount — no more "EUR 40206" beside "40.206 €".** The same trip total was written three ways across the trip page, the cruise page and the trip card; ten sites carried their own spelling, and three of them defaulted a missing currency to EUR, turning "unknown" into a claim. Every amount now goes through one formatter, and a missing currency yields a bare number rather than an invented euro (forgejo#86). - **The continents tile counts against the one shared table.** Statistiken → Flüge said "Kontinente 6/ 6" while its own caption said "of the 7" and the passport said 6/7 — three homes for one number, including a private six-bucket table with no Antarctica that filed McMurdo and Bermuda under "Other". Every airport now resolves through the same continent table the passport uses, and the denominator travels with the response (forgejo#87). - **Modal backdrops dim the page again instead of painting it black.** The add-flight form opened from the map sat on a solid black page. Its backdrop used Tailwind 3 spelling for the 50 % opacity, which Tailwind 4 drops without an error — thirteen hand-rolled backdrops had the dead class; a guard now scans for it (#289). - **The map camera stays where you left it across a domain switch.** Every dashboard tab owns its own map, and each handed MapLibre the same fixed starting view, so a user zoomed into Berlin on the flight tab landed back at zoom 2 on the cruise tab, every time — and after every save. The last camera is remembered for the session, per engine, since the flat map and the globe use different zoom scales. Deliberately not persisted across a reload; that is a product decision not yet taken (#290). - **A year with no priced flight has no cost total, not "0 €".** The cost rule started at zero and returned zero when nothing was added, a claim that the flights were free where the truth is that nothing was recorded. The tile now shows a dash with "No price recorded — 3 flights without one", and the trend pill appears only when both years have a number (forgejo#83). - **The backup routes answer with the honest status, not 500.** "That backup does not exist", "that backup is not finished" and "WebDAV was never switched on" all arrived as a server fault, filled the error log with client conditions, and the documented 404 could never be produced. They are 404, 400 and 409 now; only a share that answered badly is a 502, with the share's own words kept (forgejo#77). - **The address backfill bounds the work, not the scan.** Both nightly passes — places with a pin and no description, lodgings with a pin and no address — read a user's first 500 rows and filtered those, so an account with more complete rows than that could never get an address for anything newer; the bound had become "the first page, forever". The scan now walks in cursor pages until the candidates are in hand (forgejo#43). - **A pin in no settlement still names the place it lies in.** Five landmarks — national parks and a bay — read "—" in the place column, because the reverse geocoder consulted only city, town, village and municipality, and wilderness has none. The chain continues into hamlet, suburb, district, county and state, smallest first; the district or the state beats nothing. Rows already empty fill on the next nightly run. - **A hotel-only trip shows the country of its stays.** A trip with linked stays and no flight or cruise read "?" for its countries, because the derivation knew only airports and ports. A linked stay contributes its country the way a linked cruise does; a cancelled booking never put anyone anywhere and is skipped. - **The trend chart reads the airport clock, like everything else.** It bucketed by the stored UTC instant while every other "when did I fly" figure reads the clock at the departure airport, so a flight leaving Bangkok at 06:00 on 1 January sat in the old year on the chart and in the new year in the country list. The fetch window is widened a day at each edge so the flight is found at all, and one filter decides what is in the window for the series and the totals alike (forgejo#46). - **A cancelled house is not a visited one, and an unlock date means an unlock.** A house whose stays were all cancelled still read as visited and still proved a country to the achievements. And 103 badges stood "unlocked below their requirement" because the unlock date defaulted to the row's creation — a badge at 86 of 100 carried the day tracking began, and COUNTRIES_100 appeared to predate COUNTRIES_50. Nothing on screen was wrong; the column was. The date is now set only on a genuine unlock, cleared on revocation, and the migration nulls every row sitting below its requirement. - **A bookmarked house proves no country, so its flag goes too.** Un-marking a hotel as visited removed the country from the passport immediately while the inbox still asked about a country that no longer existed — the same two-places-two-answers fault this rework exists to remove, reappearing inside the feature built to catch it. A house's own address is still checked whether or not anybody slept there. - **The hero tile counts countries by the rule, like everything else.** Five consumers moved onto the shared evidence rule; the hero tile — which backs the Companion's start board — kept reading "countries a flight touched", so one account answered "how many countries" twice, a swipe apart, with different numbers. It was also the very number the owner was looking at when he reported the count as too high. - **Achievements no longer join country names where the passport joins codes.** One country counted several times — 88 against the passport's 32, from 56 distinct spellings for 33 codes ("Deutschland" and "Germany", "Cesko" and "Tschechien"). The name-to-code join now exists once and every country list resolves through it; a two-letter Japanese place name is no longer published as a country code. - **The parser reads the card, not the margin around it.** A Wallet screenshot yielded the status-bar clock, the word "Lufthansa" and the security number — no flight, no route, no date. Tesseract picks one global threshold for the whole image, which a navy-on-orange card in a black letterbox defeats twice. Thresholding per neighbourhood takes the twelve sample passes from 16 correct fields to 37 with fewer wrong ones. Reading more of the card also read the barcode as rubble, so printed city names now win over stray three-letter fragments, and lines of one- and two-letter debris are dropped before airport codes are harvested (forgejo#36, forgejo#37). - **The pass says which flight, not which column.** An ordinary German boarding pass came back as GRP → MUC on no date, with the boarding time for a gate and "KLASSE" for a booking reference: the date pattern demanded a clock beside it, the route took the first two three-letter tokens in reading order — which are the column headings — "Boarding" was read as a gate label, and any six-letter word was a booking reference. That last one merged a scan into an unrelated flight, since a PNR is looked up before the flight number. Known airports are preferred over reading order, a date may stand without a time, and an unlabelled reference now needs a digit (forgejo#36). - **A gate is not a flight, however OCR spells it.** Gates print as "B08" and "E06", and O-for-0 turns them into "BO8" and "EO6" — exactly the shape of a flight number, printed above the real one. Three of twelve sample passes came back as flights EO6, KII8 and BO8. Every candidate is collected and the first with a real carrier prefix wins; an unknown prefix is still accepted when nothing better is on the pass (forgejo#36). - **"ab 380 EUR" is a price, not flight AB380.** Six of eight archived Emirates promotions came back as a flight. The guard for it existed from the start and never fired: the candidate reached it in lower case, and the check sliced two letters, so "Nur 7 Tage gültig" passed as NUR7. Compared upper-cased on the whole alphabetic prefix; all eight promotions now yield nothing and every real booking still resolves (forgejo#35). - **The flight wins over the weekday and the fare beside it.** A genuine Emirates confirmation produced an invented MO24 and lost its real EK0050: the context lookahead did not cross a line break, the flight number sits alone on its line, and the line below — weekday, date, "Flughafen" — contained the word the rule wanted. Every match of every pattern is collected and one whose prefix the airline catalogue knows is preferred. Measured over 337 archived mails: EK0050, UA384, LH2040, AF1423 and others recovered; DL3, BER36, PRO1 and other phantoms dropped (#291). - **The evidence rule guards every provider, not just the fallback.** The rule that a booking must carry a flight number or both ends of a route — a date is not evidence, every marketing mail has one — lived in the template parser, which runs only when the language model is unavailable. On every instance with an LLM configured the guarded path was the one that did not run, and a "30 EUR Oster-Geschenk" promotion came back as three flights with every field empty. The rule now applies to whatever the chosen provider returned (forgejo#35). - **A mail with no booking is an answer, not a server fault.** A provider that read the mail and found nothing was filed as a provider failure; with the chain exhausted the request became HTTP 500, and the import modal sent the user looking for a broken instance. Finding nothing now returns an empty result; only a chain in which no parser ran at all still fails (forgejo#35). - **The route a mail prints in brackets is recovered.** An Air France confirmation writes each leg as "München (MUC), DEUTSCHLAND" and the model returned the legs without codes; nothing put them back although the text held them. The rule is structural — a three-letter code in brackets — because the airport catalogue contains DIE, DER, DAS, UND and BEI as airports somewhere, and the hand-kept safe list lacked Nantes, the very leg reported lost. Pairing is all-or-nothing, so a stray bracketed word leaves the route empty rather than guessed (forgejo#35). - **The hotel parser is shown the booking, not the link farm.** A 2009 hotels.com confirmation came back "no booking found", and rewriting its mixed German/English dates changed nothing. Two thirds of the mail was tracking URLs; with links replaced by placeholders the whole document parsed on the first try. The cleaning the flight parser has applied all along now runs on the lodging side too — for the model only, since a template may key on a link (forgejo#34). - **The hotel parser says who looked and what was dropped.** "No parser produced a result" read as "no parser ran", which is how a working instance came to be reported as having none; a truncated document was cut silently; and a model answer missing one required field was discarded without a word. Each is now named in the result or the log. The rule itself — a name and both dates — is unchanged (forgejo#34). - **Saving no longer collides with a badge re-check.** Every achievement was re-evaluated inside one long transaction that most call sites did not await, so two runs for one user overlapped — a flight saved while a place tick was still going — and deadlocked in Postgres, or wrote to rows deleted underneath them. The run now decides first without touching the database and opens a transaction only when there is something to write; runs for one user are chained, and the call sites wait for them, so the work cannot outlive the request (forgejo#39). - **A missing flight-data API key is reported as such**, not as "no flights found" — the lookup answers `not_configured` instead of an empty result (#232). - **Manual flight entry no longer warns about an estimate "based on boarding time"** that was never computed (#235). - **The German UI says du throughout.** Formal and informal address were mixed, sometimes on one screen; a test now reads every German string for the formal form (#236). - **Airport fields no longer render a second, empty label** holding only a required asterisk (#239). - **Ordinary marketing email was being read as a flight booking.** An Emirates promotion and an airline's holiday greeting each came back as a bookings with a reference of "LEIDER" and "NSCHEN" — the latter being the tail of "WÜNSCHEN", because the umlaut broke a word boundary and left six clean letters behind. A booking reference must now be labelled as one, and a candidate needs a flight number or both ends of a route: a date is not evidence, since every promotional email has one. - **An old mailbox imported into the wrong decade.** A 2007 confirmation became two flights in July 2026, complete with a trip named after them. A message that names only a day and a month is now read against the date the message itself carries, taken from the file rather than guessed from today. - **Re-importing the same file ended in silence.** After reviewing four legs the dialog simply closed and nothing changed, because the server had skipped every row as already present and the count was thrown away. Every outcome is now reported — imported, partly imported, all duplicates. - **Buttons that promised work they would not do.** A duplicate-only import offered "import 1 row" and imported none; the final step of a multi-leg review said "next" while it was the click that wrote everything; a mapping that produced no readable row kept its continue button live. Each now says what it will actually do. - **A lodging saved while its coordinates were being discarded.** Typing an out-of-range latitude left the number in the field, dropped it silently and saved the record without a location. All four dialogs that take coordinates now say which value is wrong and decline to save until it is fixed. - **An airport field called its own contents invalid.** Searching for something that did not exist and then picking a real airport left the "no airports found" message on screen beside a perfectly valid selection. - **The statistics and trips pages scrolled sideways on a phone.** Wide charts and the category filter row pushed the whole document past a 390px screen; they now scroll inside themselves. - **Screen readers were told the wrong thing.** Twelve controls announced either an untranslated English word or a raw internal key — including the domain switches in settings, which read "domain.flight" aloud. - **A statistics tab could be opened by URL that the account had switched off**, and the place statistics rendered for an instance where the domain was hidden entirely. - **Twenty-five new achievements spoke English on a German page.** A missing German entry fell through to English rather than to the name already stored, so "Stadtbummler" appeared as "City stroller" — with nothing broken, blank or failing to show it. - **A fresh installation created no demo cruises**, because the demo data was seeded before the ports and ships it depends on. A first start also logged errors for a handful of offshore helidecks that share an identifier, which is a property of the source data rather than a fault. - **The import log could not tell two imports apart.** Email imports were recorded without a filename, so several on the same day were indistinguishable and reverting the right one was guesswork. - **The parser availability notice contradicted the parser.** The import screen could say no AI parser was available while the very next result was labelled as coming from one; both now answer from the same place. - **A subscribed checklist reported itself complete.** 47 of 1,248 world heritage sites showed as "47 / 47 — complete", because the number beside it was never a total. The catalogue size now travels with it. - **The "All" map knows what a place pin means.** It drew places without their list's colour and, once lists could carry a symbol, without that either — so a list styled on the places map looked untouched next to the flights and cruises. The tab did not load the lists at all. Both maps in it now resolve colour and symbol through the same step, so a pin cannot show one list's colour and another's symbol. - **Airports no longer carry "(Duplicate)" in their name.** The catalogue is built from OurAirports, whose editors mark rows they suspect of being duplicates by writing the marker into the name itself. The seeder passed those through word for word, so the picker offered "(Duplicate)Vlora International Airport" and "(Duplicate)Wolf's Fang Runway" as though that were their names — a hundred and thirty-two such rows in the current file, in half a dozen bracket shapes, some of them trailing rather than leading. The marker is now stripped on import, and the entries already in your catalogue are repaired when the new version starts. The rows are kept rather than discarded: for three of these codes the marked row is the only one carrying that code at all, and one of them is the airfield the seven-continents achievement depends on. - **Addresses arrive in the alphabet you read.** Filling in an address asks a geocoder, and it answered in the local language of the place — so a German logbook collected entries in Japanese, Arabic, Armenian and Greek script: text you cannot read, sort, or type into a search box. New entries now come back in Latin script, and the nightly pass fetches the older ones again. Accented names stay exactly as they are: the line is drawn at the script, not the language, so Lëtzebuerg and Đà Nẵng are left alone. - **A country is one country, however it is spelled.** Filling in a place's address asks a geocoder, and it answers in the country's own language — so the same country arrived as "Egypt" on one row and "مصر" on the next. The places summary counted the spellings rather than the countries and reported 20 as 23, the country filter offered whichever spelling happened to sort first, and an export sheet could hold four spellings for two countries. All three now read the country code, and show its name in your language. - **Help sits on the field it explains.** The settings and the admin area opened with collapsible boxes that explained three or four fields at once, from above the form — so the sentence about a control sat rows away from the control, and collapsing the box once hid it for good. Each explanation now hangs on its own field, and what a section needs to say up front is simply visible. Anything with consequences — a restore replaces everything, a deactivated account keeps its data, debug logging costs disk — stays as standing text rather than becoming a tooltip that cannot be reached on a phone. The help icons are keyboard-reachable for the first time; they were clickable `div`s before. - **A place on the "All" map is a plain dot again**, the same mark every other domain draws, and the country of a place shows its name rather than an em dash next to a bare code. - **Every "when did I fly" figure now reads the clock at the airport.** Eight of them read the stored timestamp instead: the time-of-day buckets, weekend warrior, fastest day, milestone year, busiest month, season explorer, most countries in one day, and first airport visits — plus the year buckets on the overview tab and the country year index. A flight leaving Bangkok late in the evening could be counted onto the following day, so two figures on one screen could disagree about the same flight. Rows stored as a wall clock are no longer converted a second time, and a date-only row reports no hour at all rather than being counted on its 12:00 placeholder. - **Antarctica's airfields can be flown to.** Rothera, Marambio, Union Glacier, Novo and Teniente Marsh were not in the catalogue, so the flights could not be recorded at all. The seed admitted only large, medium and closed airports, and Antarctica — having no scheduled traffic — is listed almost entirely as small. Wolf's Fang was the lone exception, which meant "Seven Continents" effectively ran through a single airfield. The catalogue now carries 34 Antarctic fields instead of 12. Existing installations pick them up on the next airport re-sync in the admin settings. - **The Unraid install guide matched no longer-existing files.** Both template links pointed at the pre-move filenames and returned 404, and the documented `DATABASE_URL` used the database container's name — the custom Docker network is gone and the default bridge has no DNS, so it never resolved. Same defect in the Ollama endpoint. - **The flight sidebar opened on the oldest flight.** The two sort labels were swapped, and "newest first" — the default — did nothing at all. - **A bookmark to a domain page no longer bounces to the dashboard.** Opening or reloading /flights, /cruises or /lodging redirected to the start page, because the check for whether a domain is switched on ran before the list of switched-on domains had arrived. - **The dashboard no longer offers a tab and an "add" button for domains that are switched off.** - **A stay's trip field is where you would look for it.** It sat unlabelled at the bottom of "Loyalty programme", between membership numbers — the word "trip" existed only for screen readers, so the sole thing visible was the option "Not linked to a trip". It has its own section now. - **A price held on the booking no longer reads as unknown.** A flight in a package booking has no price of its own on purpose; the booking carries one all-in figure. The column said "k.A.", which claims the number is missing. It says "Pauschal" now, with the reason on hover. - **A place ticked off a checklist no longer says "Visited" above "Visits · 0".** Ticking records that you have been somewhere without claiming a date — deliberate — but the empty text denied it, and hid that photo proof attaches to a visit. - **The startup log no longer reports the wrong backup rule.** It said "no major-version bump" while the rule takes a snapshot before any version change on an existing install. It prints the actual reason now. - **A backup no longer loses every photograph.** The file archive carried three of the six upload directories, so trip photos, place photos and profile pictures were never in it. Photo records store only a filename, so a restore brought back every record with none of the images — silently, Immich-imported originals included. Backups taken before this release are incomplete and cannot be repaired after the fact. - **"Export all data" now exports all data.** It selected flights, achievements and settings while calling itself a backup in its own filename. It covers twelve domains now. - **Cruise badges and the statistics page measure the same distance.** The badge ladder measured straight lines across the map, including across land, while the statistics page measured the sea route. - **Total flight time is one number, and it says when it is estimated.** The same figure appeared three times on one screen with three results, and the average appeared twice under one label with two denominators. A flight with only a date now contributes a great-circle estimate that is counted and labelled as one, instead of counting as zero in one place and as an unmarked estimate in another. Expect these figures to rise. - **Flight costs are no longer added across currencies.** 300 USD and 300 EUR were reported as "600 €", with your own display symbol on it. Amounts are converted at the rate for the day they were spent; what cannot be converted honestly is reported per currency instead of being folded into a total. - **"Lufthansa" and "Deutsche Lufthansa" are one airline** in the headline tile, as they always were in the ranking below it. - **An empty list no longer denies the data behind the filter.** A library of 22 cruises, searched for a word none of them carries, answered "no cruises recorded yet". The list now says nothing matches, and offers to clear the filter. - **A dropped connection no longer claims the entry was deleted.** Every load failure on a detail page was reported as "not found". Only a real 404 says that now; everything else says it could not be loaded and offers a retry. - **The map key no longer covers the attribution** (#273). Covering it is a licence question rather than a tidiness one — CARTO and OpenStreetMap both require the credit to stay visible. - **The map key reads plural throughout** (#272). Its domain rows were plural while the two marker rows were singular. - **The trip insight tiles line up with the trip cards** (#271). They ran to the browser edge on a grid of their own. - **Blocking dialogs answer the keyboard.** They share one frame now: Escape closes them, the page behind stops scrolling, and focus returns to whatever opened them. - **"Duplicated" stops posing as a travel status.** It sits beside the real status as a data-quality note. - **Map markers anchor correctly on every map.** MapLibre's stylesheet is now loaded globally; a lone marker used to sit only roughly right, and several markers stacked down the page as blocks. - **A country field that names no country is filled from the pin.** A booking confirmation that prints only "Downtown Dubai" left a city standing in the country filter as its own country — no flag, one country too many in every count. Emptying a field deliberately still keeps it empty, and a country name the resolver knows is never overruled by a lookup. - **The parser judges what it is looking at, instead of only transcribing it.** The kind of place (a campground is not a hotel), the group behind a brand ("Courtyard by Marriott" -> Marriott), and the country a city sits in are now asked for. Measured on real confirmations: six campgrounds recognised as campsites, and US bookings that used to store "TX" — a state — now store the country. - **An unknown chain is offered, never created behind your back.** The import used to add any chain name it was handed, so a single run could mint entries nobody chose. The preview now flags a chain the catalogue does not know and asks once. - **The lodging filter lists countries, not spellings.** One entry covers "Deutschland" and "Germany", named in the reader's language. - **A hotel confirmation could not be read at all once a flight had been parsed before it.** The lodging and cruise parsers asked the language model for one context size and the flight parser for another; a model is loaded per context size, so every alternation forced a full 9.9 GB reload. Measured against a real host: a request matching the loaded model answers in 7 seconds, one that forces the reload did not answer within 240 — well past the parser's own limit, so it gave up and offered manual entry. All three now ask for the same size. - **The total on a confirmation is read from the document, not guessed.** Two identical runs of the same booking returned the full amount once and the bare room rate the other time — a difference of 2,142 AED on one stay. A total the document actually labels now overrules the model. A number counts as money only if it carries a currency or two decimals, so "(1 Erwachsener)" beside the word "Gesamtpreis" stays a guest count; and the amount belonging to a label is the nearest one, not the last. - **Confirmations are read for what they say.** The meal plan, the per-night rate and how many people the booking covers were never asked for, though 61 %, 47 % and 42 % of real confirmations state them. A booking for more than one person now points at the companions field — it never invents a name, because a confirmation names the booker, not the companion. - **A country is a country, whatever language wrote it.** The lodging filter listed 60 entries for 33 countries, "Deutschland" beside "Germany". Grouping now runs on the ISO code; the original text is kept untouched as the record of what the source said. - **The import looks for the house before creating it a second time.** It compared names and cities only, so one building became two records whenever a booking mail and a saved-places export wrote it differently. It now also compares where the house IS, and reads through decoration like "Hotel", "Restaurant" or "GmbH". - **A house number goes where the country puts it** — "50 Southwest Morrison Street", not "Southwest Morrison Street 50". - **A total that leaves entries out now says so** — on the stay, on the lodging, in the list and in the statistics strip, rather than quietly summing what it could and presenting it as complete. - **The lodging list stopped at 200 rows without a word.** With more hotels than that, everything past the cut simply did not exist for the list — while the counter above it reported the true number. - **A price whose currency was thrown away is no longer read as euros.** The API refuses an amount without a currency instead of guessing one. - **"Länder besucht" counts every country a flight touched** (#233), not only the ones it started or ended in. - Numerous smaller corrections from the beta review: a stay priced only per night now reaches the trip total, the overall rating is derived on every write path, a hotel-only trip shows its cost, and the import surface no longer has dead ends. - **Clearing a location no longer invents a new one.** Emptying address, city and country handed the geocoder the hotel's NAME and adopted whatever came back, so a cleared row returned with a different address and a matching pin somewhere else entirely. A deliberately emptied field is an instruction, not a gap. - **A flag for a country that names itself in its own language.** The resolver understood German and English, while imported data arrives as "Italia", "Sverige", "Việt Nam" or "Schweiz/Suisse/Svizzera/Svizra". It now reads a country name in any language the runtime knows — the interface stays German and English; those are different questions. The flag also NAMES its country now, as a tooltip and to screen readers, instead of being a decoration they were told to ignore. - **A hotel check-in no longer sorts above the flight that brought you there.** A stay carries no time of day and is stored at midnight, which the timeline read as 00:00 — the earliest moment of the day — rather than as unknown. - **The lodging form scrolls.** With the map picker or the advanced panel open it grew past the viewport with nothing to scroll and the save button out of reach. Its map now follows a search instead of staying on the world view with the pin off-screen, and deliberately does NOT follow a marker drag. - **Sample documents can no longer be committed** — the ignore rules covered mail files but not documents. - **Flight arcs start at the airport again.** Every flight kept its own copy of the departure and arrival coordinates, written by whichever source created it — and providers quote different reference points for the same airport, so one airport sat in several places at once. Measured on one account: 58 of 694 references disagreed with the catalogue, the worst by 1.6 km, which drew arcs that visibly missed their own airport dot. The catalogue is the authority now, both when drawing the map and when writing a flight. - **The map says what is clickable.** Airport dots, route arcs, ports and lodging pins all open something on click; the pointer never showed it. - **Beta features can be switched on from the admin surface**, instead of only through an API call. - **Unticking a chain no longer looks like deleting the card.** A loyalty programme leaves that chain's page by design — it now says so before you save, and names where the card stays reachable. - **Names on the trip map thin out with zoom**, as they already did everywhere else. - **Something planned is no longer counted as done.** One tooltip reading "1× geflogen" over a route nobody had flown yet led to thirteen places across every domain with the same blindness: countries "visited" by a scheduled flight, cruise figures counting bookings, cruise achievements unlocked by booking alone, a chain's average rating including unvisited stays, "last visit" dates in the future, flown-km sums including planned flights, and trip superlatives crowned by a merely planned trip. Every count, label and unlock now asks whether the thing actually happened; a mixed route says "3× geflogen · 1× geplant". The itinerary map of a booked cruise keeps its port pins — it just no longer claims you have been there. - **A brand-new account is not greeted with old news.** Signing up put "New in TravStats 2.6.0" in front of someone who had never run 2.5. The running version is now recorded as seen when the account is created — on registration, admin-created users and first-boot setup alike. - **An untouched flight form no longer classifies the flight.** Seat class and category were silently preselected ("Economy", "Business"), so a never-classified flight was stored as an economy business trip. Both start empty now, and the settings page gained an explicit "no default" choice. - **A selected flight no longer follows you into another domain.** The popup card and its selection rings survived the dashboard tab switch and kept rendering over the lodging map. - **The login page stops advertising a locked door.** With registration disabled the "Register" link is hidden; invitation links still work, and the rejection now arrives in the UI's language. - **A lodging without coordinates is no longer a dead row.** The dashboard list links every lodging to its detail page and marks the unlocated ones; "no location on file" on the detail page now offers to search one right there. - **The place search can no longer fail silently.** The OSM request now identifies itself (it sent no User-Agent — exactly the traffic public OSM infrastructure blanket-blocks), retries without an unsupported UI language instead of returning nothing, tolerates a base URL already ending in /api, and a geocoder outage now reads "search unavailable" instead of the misleading "no results" — including in the admin connection test. - **Messages arrive in the UI's language.** API-key test results and the registration rejection were English prose in a German UI; they now travel as message keys the frontend translates. - **Ones are singular.** "1 Übernachtungen", "1 Hotel(s)" and friends now inflect properly, and the multiplier reads "×" everywhere. - **The flights tab greets an empty logbook** with the cruises tab's empty state — what the tab shows, plus both ways to get a first flight in; the lodging tab's card gained the same call to action. - **Airport search stopped ranking closed heliports.** "GRU" returns São Paulo instead of nine closed US strips whose identifiers merely contain the letters; closed airports stay findable by their exact code. - **Small honesty fixes across the UI.** Tab counts say "(n planned)" so they stop contradicting the flown-only statistics; the settings footer claims auto-save only where auto-save exists; the whole domain tile toggles; the flight-number search date field got a real label; the support menu speaks the UI's language. - **SMTP credentials can be deleted again.** The mail settings offered no way to take a stored password back out — the toggle only stops sending, and saving can overwrite the secret but never clear it. A delete button now appears wherever something is actually stored, behind a confirmation that names the consequence, and clearing the configuration empties the form with it. - **The beta-features switch applies immediately.** Turning it on or off used to reach the gated surfaces — the Devices entry, the POI dashboard tab, the trip summary card — only after a full page reload. - **The parsers stopped filling in what the document does not say.** A cruise confirmation without any flight numbers no longer arrives with an invented one; a booking's price is the total actually charged for everyone after discounts, not one guest's pre-discount fare; a two-digit year in a hotel confirmation is read against the travel period rather than the letter date, and only an explicitly labelled booking number is taken as one. - **Countries are counted once, however they were spelled.** The lodging statistics grouped by the country text a booking mail happened to use, so "Deutschland" and "Germany" appeared as two countries with the nights and the money split between them — in the country list, the price and rating rankings, and the cross-domain "countries visited" tile, where flights and cruises had always contributed codes. Continents were affected too: the lookup understands codes and English names, so German text fell back to a coarse coordinate guess, and a house without coordinates lost its continent entirely. - **Flight totals count flights that happened.** The year card put a flight count covering booked and cancelled flights next to a distance and a flight time covering only the flown ones, and the mobile app's home board could report one flight with zero kilometres and zero airports. Every headline figure now describes the same set, and what is merely booked is reported beside it instead of inside it. - **"Aktive Reisetage" counts days, not one tally per domain.** A day with a flight and a hotel night counted twice, and the figure could pass 365 in a year. - **A stay without a date is no longer called a double booking.** The rhythm section reported undated stays as "n Nächte doppelt gebucht"; it now measures overlap against the nights that can be placed on a calendar at all, and says plainly how many stays it had to leave out. - **The lodging statistics page stopped covering itself.** The figures strip and the currency card float over the dashboard map by design; on the statistics page they sat on top of the tiles below, and the translucent currency card let the hotel names show through. - **Confirmation dialogs answer the mouse again.** An invisible backdrop lay over every dialog built on the shared component: the click landed on the backdrop, which closes the dialog, so confirming looked like it had worked while nothing happened — including the SMTP deletion this release introduces. Dialogs also close on Escape now and announce themselves to screen readers. - **The map picker opens where the point is** — it showed the world view whenever the position arrived while the picker was shut, leaving the pin off screen — and it lists the places around a pin the search placed, not just one the map placed. - **Amounts follow the interface language.** Only seven currencies carried a locale, so for the other 148 the amount was written in the browser's language while the exchange rate beneath it followed the interface — two number formats in one card. - **Smaller counting corrections.** Round trips are limited by the thinner direction (three flights out and one back is one round trip, not two); the continent tile stopped counting its own "other" bucket as a continent; the revisit rate no longer treats an unresolved port as a return visit; the sea-day share cannot exceed the days sailed; a flight with no airline is not ranked as an airline called "Unknown"; cruise lines are ordered by how often they were sailed; and the country distribution card says "origin and destination", which is what it has counted since 2.5. - **Achievements react to catalogue edits again.** Points, tiers and wording were frozen on any installation whose stored achievement count happened to match the seed count. Two badges also corrected themselves: the loyalty badge sat at gold between the bronze and silver rungs of its own ladder, and a distance badge claimed 50,000 km is the Earth's circumference. ### Security - **The page carries a Content-Security-Policy.** Helmet set one, but only on `/api` responses, which govern nothing that matters for script injection — the HTML that decides whether injected script may run was served with no CSP at all. `script-src 'self'` with no `unsafe-eval`, measured against the production bundle rather than hoped for; `connect-src` stays deliberately broad because a self-hosted instance talks to the Immich, Dawarich or OSRM the operator names, often over plain http on a LAN. Verified in a browser — the first attempt silently blocked the fonts and Leaflet's stylesheet, which no reading of the header would have shown (forgejo#29). - **A deactivated account is refused a session, on every path.** Login answered a disabled account with HTTP 200 and a fresh cookie; the lock held on the next request, but the door still handed out a key. Five routes mint a session — login, registration, first-boot setup, two-factor verification and passkey assertion — and all five now go through one function that refuses an inactive subject. The two-factor branch is guarded separately, so a disabled account no longer receives a "password accepted, now finish signing in" (forgejo#31). - **Every unlimited route now carries a verdict.** The rule "rate limiting on every expensive endpoint" was practised but unreadable: a reader could not tell "considered" from "forgotten". Twenty-five routers now carry a limiter or one sentence saying why they need none. Limited on the way, by cost: the Immich photo-journey scan (the most expensive request in the app), CSV preview and parse, training and photo uploads, the cruise geometry batch, the achievements leaderboard — the one route whose cost scales with the instance rather than the caller — and the checklist suggestions (forgejo#51). - **Rate limits count per user, not per address.** The flight-lookup, analytics, PDF-parse and boarding-pass limiters sat behind authentication but were keyed by IP, so a household — or every user behind one reverse proxy — shared a single bucket and one person's lookups blocked everyone on the same connection. A guard now demands the per-user key on every limiter that runs after authentication (forgejo#68). - **Refreshing the airline templates is an admin action.** Any signed-in account could replace the template registry every user of the instance parses with, and spend the instance's three GitHub refreshes per hour. The status stays readable by everyone — which airlines the mail parser understands is a user question — and the page explains why the button is missing instead of offering one that always fails (forgejo#67). - **The data export no longer carries stored API keys.** It pulled the entire settings record into a downloadable file, including OpenAI, Claude, AirLabs, Aviationstack, AeroDataBox, OpenSky and Immich credentials. They are encrypted at rest, but nothing in an export needs them. - Closed an IDOR in the lodging import commit path, scoped receipt access to the owner, and hardened the import HTTP surface (four findings from a scoped review). - **A read-only access token could change flights.** The pending-updates routes checked only that a caller was authenticated, so a token issued for reading could apply, edit or delete a pending update — and applying one writes to the flight underneath. Those routes now require write scope like every other mutating route; read tokens keep their read access. ## [2.5.2] - 2026-08-12 Hotfix release. Three ways live flight data went wrong, no database changes. ### Fixed - **Flight numbers printed with a leading zero found nothing at all.** Airlines write "EK051" on the itinerary and that is what gets stored, but every flight-data provider keys on the unpadded form, "EK51", and answers the padded query with an empty result. Live tracking, automatic updates and enrichment therefore did nothing whatsoever for Emirates, Qatar, Gulf Air, EgyptAir and every other airline that pads — silently, because "no data" looks the same as "nothing to report". Requests now drop the padding while the stored number keeps your spelling: the answer is mapped back onto it, so an automatic update cannot quietly rename your flight to the provider's convention. - **Departure and arrival times were an airport's offset too early.** AirLabs returns both a local and a UTC time, and neither carries a zone marker, so the UTC value was read a second time as local — an Emirates flight leaving Dubai at 15:55 was recorded as departing 07:55 UTC instead of 11:55. The UTC values are now marked as such before conversion; times that only exist as local are still converted as before. - **Live tracking could move a flight to the previous day.** Flight-data providers identify a rotation by its LOCAL departure date, but the automatic update asked for the UTC day of the departure instant. For any early-morning departure east of Greenwich those differ — a Sydney 06:00 departure is still the previous day in UTC — so the update fetched the previous day's aircraft and, with approval disabled, rewrote the flight a full day into the past, actual times and "flown" status included. Lookups now use the departure airport's local calendar day, and any API answer whose scheduled departure is more than 12 hours away from the stored one is rejected as the wrong rotation instead of being proposed at all. - **The built-in parser misread Emirates bookings.** The itinerary tables put the time, the weekday and a two-digit-year date on three separate lines — the parser required four-digit years and never associated the times, so the actual legs came out dateless. Worse, the legal footer ("Erlass Nr. 2 von 1985") parsed as the flight date 1985-01-02, because an unrecognised month silently became January. Month names are now validated, implausible years are rejected, two-digit years are understood, and a time on the line above its date is paired with it. - **Very long booking emails were silently cut off before the AI parser saw them.** Anything beyond 5,000 characters was dropped without a trace — and return legs live at the end of an itinerary. The window is now 12,000 characters and a truncation is logged. ### Upgrade notes - No database migrations in this release. - Flights that live tracking already shifted to a wrong day are not corrected retroactively — check recent long-haul arrivals whose departure was an early morning east of UTC. - Times already stored an offset too early are likewise not corrected retroactively. Re-running a lookup on an affected flight now returns the right value. ## [2.5.1] - 2026-08-09 Patch release. Three fixes, no database changes. ### Fixed - **The aeroplane between two airports flew nose-down.** The glyph in the flight table's route column was rotated by a further 45 degrees although it already points due east, so a page of flights read as a page of crashes. It flies level now. - **An ordinary action wrote to the error log.** Adding a companion you already have was implemented as "create the record, and put it right when that fails", so normal use filled the error log with entries that described nothing wrong — 83 of them on a single start. It is one operation now, and the log stays quiet. - **A mistake in a request is no longer recorded as a fault of the server.** A rejected form or an expired session was written at error level, exactly like a crash, so the log could not answer whether anything was actually broken — and anyone who could reach the port could make it grow without logging in. Genuine server faults still log as errors; everything below them is a warning. ### Upgrade notes - No database migrations in this release. ## [2.5.0] - 2026-08-09 Large minor release. Trips can pull in your Immich photo albums, travel companions become real records instead of loose text, airlines and aircraft get a proper catalogue behind the scenes, the flight list is rebuilt as a departures board, adding and editing a flight finally use the same form, and status stops being something you set by hand. ### Added - **Immich photo albums on a trip** (#154). Link an album from your own Immich server in one of two modes: _linked_, where the images stream through an access-checked proxy and take no storage here at all, or _imported_, where the originals are copied in as ordinary trip photos. The gallery groups by album, any picture can become the trip's cover, and a re-sync collects stragglers. Photos run oldest first and divide into days, so an album reads the way the journey did, and the grouping can be switched off. The key field names the read-only permissions it needs, so nobody has to hand over a full-access key. The album picker has a search filter (#181), and an administrator can configure one instance-wide connection (#182). - **Travel companions are real entries.** Co-passengers used to be free text on each record. They are now created once and reused everywhere — flights, trips, cruises and special flights all share the same picker. Existing entries are migrated on first boot. - **Airline and aircraft master data** (#189, #191). Both get a catalogue with the same admin treatment ships and ports already had, seeded from OpenFlights and searchable from the flight form. Airports join the same page. - **Airline logos as brand tiles.** The flight list shows each airline as a logo tile, resolved through a four-tier chain with a keyless default, cached on disk and refreshed nightly. - **The flight list is a departures board.** Departure and arrival share one time column, country flags mark the route, actions are icons, and a dot on the row says where the data came from. - **One flight form, with scheduled and actual times** (#199, #200). Adding and editing use the same mask. New: scheduled vs. actual times with a derived delay — the recorded time sits beside the scheduled one in the flight list, red when later and green when earlier — plus boarding group, trip assignment, cost and receipt. - **Status is derived from the dates** — for flights, cruises and trips alike, re-derived hourly. Only "cancelled" is still set by hand. - **Grouped navigation and a central import hub.** The desktop bar gains Logbuch/Support/System submenus; every import route now starts in one place. - **Prices belong to a booking.** A multi-leg booking carries one price instead of a repeated per-segment total, editable from the trip page, with per-currency totals ("EUR 300 + USD 480") rather than naive summing. - **The trip timeline rail is coloured by progress** (#184) instead of graying out entries. - Aircraft statistics rank types rather than tail numbers, the airline ranking carries its IATA code, and the achievements summary carries a rank. ### Fixed - **Settings marked "auto-saved" that were not** (#198). Flight defaults now actually persist — and clearing a field clears it, in every form. - **An expired session showed the dashboard and a shower of 401s** instead of the login page. - **A flight more than 30 minutes late lost its arrival permanently.** It now keeps chasing it. - **A half-imported airport catalogue rendered UTC as if it were local time.** Re-seeding now leaves a usable catalogue behind rather than just rows, and a closed airfield is never picked as the nearest airport. - **"Countries visited" ignored the selected year** and carried a year-over-year comparison that could only ever read zero. It is year-scoped now, and a country reached both by air and by sea counts once, not twice. - **A blank time was silently stored as noon**, producing invented delays; a recorded cabin class was priced but never saved. - **A diary entry's preview printed its own markup** (#231). The timeline card showed `**bold**` with the asterisks intact while the same entry rendered properly once opened. The preview now renders, with headings and lists flattened to fit a two-line card; the headline above it is stripped to plain text instead. - **The uploaded-photos box no longer lingers** once an album is linked (#179). - Trip aggregates use airport-local times and see manually added flights. - **A trip's card and its own page disagreed.** The card read "?" countries and no total where the trip's page read five countries and a price. The country fallback had only been taught to the detail endpoint, and the card's total was hidden behind the cost-tracking toggle — which since #192 governs the taxes/fees breakdown, not whether a price is shown — while also ignoring prices entered on a flight rather than a booking. Both surfaces now answer from the same sources, and a trip made only of cruises takes its countries from the ports it called at instead of showing "?". - **The cruise tab counted one country too many.** The port catalogue lists both "United States" and "United States of America", so counting names counted the same country twice. The tally folds them; the country list below still shows the names as recorded. - **The built-in demo account is now marked as one.** Created through the Docker demo-user switch it was not, so its sample flights and cruises counted towards the instance-wide statistics as though they were real travel. Existing installs are corrected on startup. - **Aircraft types are written consistently.** A library built up over years could hold "Airbus A350-900", "B737-800" and "A320neo" side by side in the same column; only newly entered flights were ever normalised. Stored types are brought to the catalogue's spelling on startup, and a type the catalogue does not know is left exactly as you typed it. - **Cruise regions are translated, all of them.** Ten regions had German names hardcoded while the port catalogue ships fifty-four, so a German page showed "Mittelmeer" and "Ostsee" next to "North Sea" and "Iberian Atlantic" — and an English page got the German names for those ten. All fifty-four now exist in both languages. - **Two airline rankings on the statistics page disagreed.** One counted only the flights actually flown, the other counted every booking including cancelled and still-scheduled ones, so the same airline appeared with two different totals a few centimetres apart. Both now count flown and historical flights, the way every other statistic does — and a cancelled flight no longer adds its distance to an aircraft's lifetime total. - **Cruises counted towards neither the distance nor the cost of a trip.** A trip made only of cruises showed no kilometres and no total although the prices and the computed sea routes were on file. Cruises now count exactly as flights do, on the trip card and on the trip's own page. ### Security - react-router 6 → 7, closing the open-redirect advisories, plus every other open dependency alert in the app and in the Discord tool. - Secrets shorter than 16 bytes were returned as ciphertext by a length heuristic; upload-cleanup paths were hardened; API-key material no longer reaches log messages. ### Upgrade notes - **Seven database migrations** run automatically on first boot of this version. Take your usual backup first. - The Docker image now vendors the airline logo set and the OpenFlights seed data; no configuration change is required. - A one-shot backfill migrates existing companion text and booking prices at startup. - Two further one-off repairs run at startup alongside the migrations: stored aircraft types are normalised to the catalogue's spelling, and an existing demo account is flagged as a demo account. Both are safe to re-run and skip what they do not recognise. ## [2.4.0] - 2026-07-11 Minor release. Six reported bugs, an overhaul of how the map is coloured, and a switch that keeps unfinished features out of sight until they are ready. ### Added - **Map colouring is now an explicit choice, for flights and for cruises.** Previously the colour picker was quietly overridden by whichever view you were in, so picking a colour appeared to do nothing at all. Both domains now offer the same three modes: **Status** (two colours — flown/sailed vs. planned), **Frequency** for flights (one base colour, deeper the more often you fly a route) or **Per cruise** for ships (a distinct colour per voyage), and **Solid** (one colour for everything). The legend follows the mode you pick, and status mode gives you a separate colour picker per state. - **Flat route shape for flights** (#183). Flight routes can now be drawn flat on the map like cruise routes, instead of as 3D arcs. Arcs look better from a distance, but their endpoints drift away from the airports as you zoom in; the flat shape stays accurate at any zoom. - **Beta-features switch.** Features that are not finished yet are now hidden by default and can be revealed instance-wide by an administrator. On a fresh install this hides the POI dashboard tab, the Devices settings page and the trip AI summary. - **The profile picture can be removed.** A Remove button next to the file picker deletes the stored picture; the initial-letter avatar returns. - **Flight price and currency are always editable** (#192). The fields sat behind the cost-tracking toggle while cruises always showed them — an imported price was visible in the flight list but not editable. The toggle now only gates the taxes/fees breakdown and the business statistics. - **The map control panel starts collapsed and remembers its state** (#194). It no longer springs open on every reload; opening or closing it persists across reloads and the 2D/globe switch. - **The settings domain switch moved into the sidebar.** As a top-right pill row it was easy to miss; on desktop it now sits directly above the section list it scopes. On phones it stays in the top bar. - **Booking reference and ticket number when adding a flight manually** (#197). Both fields only appeared in the edit dialog after a flight was saved. The manual add form now offers them directly, and the booking reference is uppercased on entry so manually added flights group into the same booking as imported ones. - **The What's New dialog covers 2.4.0.** A short in-app summary of this release's changes (in German and English), shown once after updating. ### Fixed - **Cruise port names with non-English characters render correctly** (#185). Ålesund, Flåm, Málaga and Kuşadası previously drew as empty boxes on the map. - **Cruise direction arrows no longer sit inside the ports.** They were anchored at a fixed position in the route's point list rather than along its actual length, which on a short leg put them on top of the harbour. They now sit at even intervals along the leg, and very short hops get none. - **The profile picture and birthday are saved** (#186). Uploading a picture had no backend endpoint at all, and typing a birthday never triggered a save — while the page still displayed "auto-saved". Both now persist. - **Airport and port dots are the same size** on the flat map (#187). - **The statistics "compare with" year survives a page reload** (#188). - **Airlines are shown by name, not by code** (#178). ICAO codes now resolve to full names, from the same catalogue the backend uses. - **Achievements: continents are resolved by country.** The Arctic was counted as Antarctica (which made Svalbard flights unlock an Antarctica badge) and Australia as Asia. Badges that no longer qualify are now revoked instead of staying unlocked forever. - **Direction arrows on out-and-back cruise routes no longer stack into an "X".** When the same water is sailed in both directions (there and back on one route), the two midpoint arrows landed on exactly the same spot. They now flank the midpoint, each pointing in its own direction of travel. Arrows also render sharp at every size — they were rasterised at their native size and went blurry when scaled up on high-DPI screens. - **The admin instance-settings form no longer resets while you type** (#190). Every keystroke re-triggered the form's initial load, which overwrote the input with the server's values — editing the public URL was impossible. The underlying cause (an unstable translation-function identity re-firing effects on every render) is fixed app-wide. - **Uploading a profile picture no longer ends in "Not found".** The web server's static-asset cache rule intercepted API URLs ending in an image extension, so the freshly uploaded picture 404'd before the request ever reached the backend. Image receipts were affected by the same rule. - **The year dropdown in the map panel is readable again** (#196). On Windows the dropdown popup rendered white-on-white; the options now carry explicit dark styling. The globe's performance dropdown had the same flaw. - **The map panel chevron points the way the panel moves** (#195) — down while open, up while collapsed — and is larger. - **Dark text on amber buttons** (#193). The trip module's primary buttons referenced a CSS variable that does not exist, so their text silently rendered near-white on the amber accent; several admin and cruise buttons hardcoded white. All of them now use the same dark text as the standard primary button. - **Every button in the app follows the same system.** A dozen controls still carried hardcoded blues, greens and oranges from before the design system existed — the admin API-key and parser saves, the airport-seeding dialog, the pending-update card and editor, the flight review submit, the duplicate warning and the delete-demo-user button — so on those surfaces the loudest control was the off-brand one. They now use the shared primary, secondary and danger classes and follow the accent colour. Two were also wrong on the merits, not just off-palette: the pending-update card highlighted **Edit** while **Apply** — the action the card exists for — sat in green beside it, and the duplicate warning painted the risky "add anyway" escape hatch louder than the recommended merge. In both, the recommended action is now the primary one. Deleting the demo user and rejecting a pending update are destructive, and now look it. - **An update now actually reaches the browser.** The app shell (`index.html`) was served without any caching instructions, so browsers were free to reuse their copy for hours without asking the server. That stale copy still points at the previous build's JavaScript files: either they are still in the browser's cache and you keep running the old frontend against the new backend, or they have been evicted, the request 404s and the page stays blank. The shell is now revalidated on every load, while the versioned assets keep their long cache. If you have ever had to hard-refresh TravStats after an update, this was why. - **Cruise direction arrows no longer pile up on shared shipping lanes.** Different cruises routed onto the same lane each placed their own arrows, stacking into unreadable clusters. Overlapping arrows are now hidden and reappear as you zoom in; a selected cruise always keeps its own. ### Changed — please read before updating - **Flight colouring:** existing settings migrate to **Status** mode. The combined view looks unchanged; the flights-only view switches from the frequency ramp to the two status colours. Switch back to **Frequency** in the map panel if you prefer it. - **Cruise colouring:** the Cruises tab no longer colours each voyage automatically. It starts in **Status** mode — pick **Per cruise** in the map panel to get the old look back (one click, and now discoverable). - **Hidden by default:** POI tab, Devices settings and the trip AI summary now require the beta-features switch. ### Removed - **The "Map settings" section is gone from the flight settings tab.** Base map, start zoom, marker style and route colour had no effect anywhere since the in-map control panel took over — every control there was a silent no-op. ## [2.3.1] - 2026-07-11 Patch release. Fixes the release-highlights dialog, which never appeared in 2.3.0 — and with it the usage-statistics consent prompt, so nobody was ever asked. ### Fixed - **The "What's new" dialog now actually appears.** In 2.3.0 the dialog was matched to the running version by an exact string comparison, and its only entry was tagged with the version the feature had originally been planned for. On a 2.3.0 installation nothing matched, so the dialog stayed hidden — taking the anonymous-usage-statistics consent prompt with it, since that prompt is shown inside the dialog. Highlights are now matched to any version at or above the one they describe, so the dialog also reaches someone who skips several releases at once, and dismissing it is remembered across later patch updates. - **Database index name reconciled with the schema.** A hand-written migration had created the trip-photos index under a different name than the schema expects, so every newly generated migration on every branch carried a spurious rename. Renamed once (metadata only — no data is touched). ## [2.3.0] - 2026-07-11 **TravStats 2.3.** Aggregates the entire `dev/v2.3` forward line — a redesigned map/globe appearance system, per-cruise and status-based map coloring, a new statistics scorecard, unresolved cruise ports, device pairing for the companion app — and adds opt-in anonymous usage statistics with a What's-New dialog. ### Added - **Anonymous usage statistics (opt-in)** — TravStats can now optionally report a small, anonymous usage summary to a public dashboard at `travstats.de/stats`, so the community's reach is visible. **Off by default** and admin-controlled: a consent choice appears during first-boot setup and in the What's-New dialog, with a permanent toggle under Admin → Instance. The payload is coarse and anonymous — version, enabled domains, rounded distance totals, feature usage — and **never** IP addresses, names, travel details, or API keys. Withdrawing consent asks the server to erase this installation's record. Self-hosters can disable all sending with a single environment variable. Full transparency page at `travstats.de/docs/usage-statistics`. - **What's-New dialog** — after an update, a one-time dialog summarises what changed in the new version. - **Continuous map sliders** — line width and marker size are now stepless sliders with a live value (replacing the fixed Thin/Normal/ Thick and Off/S/M/L presets), on both the 2D map and the globe, plus a new **cruise route-arrow size** slider (0 hides the arrows). - **2D-map hover tooltips match the globe** — airports, ports, flight routes and cruise routes now show a country flag, ICAO/IATA code, place and count on hover. - **POI/stop labels on the trip map** — trip stops (POIs, hotels, …) show their name on the map instead of a bare coloured dot. - **Redesigned map & globe experience** — consolidated appearance control panel shared between the 2D map and globe, mode-aware presets (marker size, route width, basemap parity), readable port/airport labels with priority reveal on zoom, flag-based markers (flagcdn, not emoji), and an activity histogram with playback replacing the old time slider. - **Per-cruise and status-based map coloring** — cruises can now use a distinct color per trip or the default two-tone (sailed/planned) status coloring; flights get a warm/cool status split (flown/upcoming) in the "All" view, with intensity scaled by route frequency. - **Statistics: KPI scorecard & trend charts** — a new overview scorecard (sparklines, time-range control) and a canonical flights-per-period chart, backed by a new `GET /stats/timeseries` endpoint. - **Unresolved cruise ports** — an imported stop that can't be matched to the port catalog is kept as a port call with its original name (🔶 in the timeline) instead of being silently downgraded to a sea day; resolvable later from the stops editor. - **Cruise table improvements** — sortable columns, inline duplicate/edit/delete actions, and country-flag chips per cruise. - **Device pairing for the companion app** — secure claim-code flow (start/status/claim/unpair) with a live QR on the new Settings → Devices page, plus per-device metadata on API tokens. - **Cross-device app settings sync** — `GET/PUT /api/v1/app-settings` persists mobile-app preferences server-side. - **Boarding-pass QR from photos** — digital boarding passes can now be decoded from an uploaded image, not just a live scan. - **Trip diary entries** — a read-only diary view with Markdown rendering on the journey detail page. - **Discord community integration** — a community link in the app header, plus release/beta/RC announcement tooling. - A proper 404 page for unknown routes (was a blank page). ### Fixed - **Admin backup page no longer scroll-jumps** — the backup screen's 5-second status poll was flipping the full-page loading state on every tick, blanking the table and snapping the scroll position back to the top every few seconds; the poll now refreshes the list silently (#180). - **Loading labels showed a raw translation key** — several admin and app loading states displayed the literal text `common:loading` instead of "Loading…", and two "close" controls (including a screen-reader label) were likewise unresolved. All now show the correct text, guarded by a test. - Cruise stops with an ambiguous name (e.g. "Naples") now resolve to the correctly-catalogued port instead of a same-named placeholder in another country (#169). - The map colour legend now sits in a compact bottom-right table, so the globe's activity time-histogram no longer overlaps it on the "All" tab. - Cruise-arc arrows now point in the correct travel direction and have a bordered, easier-to-read shape (Discord bug report #160). - Overlapping airport/port labels in dense map clusters are decluttered. - The stats overlay no longer collides with the map's Add button. - Time histogram: proportionate collapsed layout, a properly positioned year axis, confusing speed selector removed. - Statistics trend deltas show percent-only and hide cleanly when there's no comparison window; all-time charts trim empty leading buckets. - Various cruise-stop edge cases (unresolved-port singular/plural copy, sea-day toggle clearing stale names, import-preview wording). - Boarding-pass duplicate detection is now timezone-aware. - AI trip-summary generation uses the long parser timeout instead of a 10s default. ## [2.2.2] - 2026-07-04 ### Fixed - **No phantom vertical scrollbar on horizontal tab bars** — the trip-detail, admin, statistics and parser tab bars no longer show a stray vertical scroll bar; horizontal scrolling on small screens is unaffected. (#155) - **Bug reports attach the diagnostic bundle as a file** — "Report a bug" now downloads the bundle for you to drag into the GitHub form instead of copying it for a paste that GitHub rejects as too large. (#157) - **Admin panel shows the clean release version** — after switching an RC image back to the release tag, the panel no longer looks stuck on the RC version; the exact build is shown on hover. (#156) - **Correct English comparison label in statistics** — the year-over-year delta badge showed the German "ggü." in the English UI; it now reads "vs". ## [2.2.1] - 2026-07-04 ### Fixed - **Uploaded photos now survive container updates** — Trip photos, receipts and imported files were stored inside the container instead of on the mounted data volume, so they disappeared on every image update. Uploads now live on the persistent `/app/data` volume automatically, with no extra bind-mount required; an existing manual upload mount is detected and left untouched. (#152) - **Large photo and PDF uploads no longer fail** — The built-in web server capped request bodies at 1 MB and rejected larger photo or PDF uploads with a "content too large" error. The limit is now 100 MB, in line with the file sizes the app already accepts. (#153) - **Trip timeline dots align on the connector line** — On a journey's timeline the event dots sat slightly off the vertical line at an inconsistent height and were drawn as hollow rings. They are now filled, centred on each event card, and sit exactly on the line, which runs cleanly from the first to the last event. (Discord bug report) - **Correct plurals for trip counts** — Trip counters always showed the singular form (e.g. "532 Nacht", "4 Flug"). German and English plurals now render correctly ("532 Nächte", "4 Flüge"). (Discord bug report) ## [2.2.0] - 2026-06-14 ### Added - **Cruise itinerary dates** — Each cruise stop now stores its calendar date, extracted from booking confirmations on import (even when no arrival or departure times are listed) and editable per stop, including sea days. Stop dates appear on the cruise detail timeline. (#132) - **Cruise route name** — Cruises gain an optional route/itinerary name (e.g. "Kanaren mit Marokko") as printed on booking confirmations, captured on import and shown in the cruise list and detail header. (#133) ### Fixed - **German and local port names now match on cruise import** — The importer resolves German exonyms ("Lissabon", "Singapur", "Venedig") and local endonyms ("Lisboa", "Roma", "Napoli") to the correct catalog port instead of leaving them unmatched, with a fuzzy fallback for near spellings. Rome and Florence map to their actual ports of call (Civitavecchia, Livorno), and a short catalog name can no longer be mis-matched to a longer parsed name ("Atlantis" no longer matches "Atla"). - **Airport selectors no longer auto-expand on modal open** — Pre-filled airport fields (such as the fly & cruise import preview) only open their dropdown when focused, instead of all popping open at once when a dialog opens. ### Changed - **Consistent "Add flight" button placement** — The flights page now shows a heading with the "Add flight" button right-aligned, matching the cruises and trips pages. ## [2.1.0] - 2026-06-13 ### Added - **Worldwide port catalog** — The cruise port catalog grew from ~670 to over 12,000 seaports (imported from the public UN/LOCODE dataset), so ports like Taranto that were previously missing now resolve directly in manual entry and import. - **Port map-search fallback** — When a port isn't in the catalog, the picker looks it up via OpenStreetMap and fills in its coordinates automatically instead of forcing manual latitude/longitude entry. ### Fixed - **Cruise dates were lost or shifted** — Entering only a date in the cruise form left it unsaved (so it never appeared in the overview), and entered dates could drift to the previous day across timezones. Cruise start/end are now date-only and timezone-stable. - **Accented ports weren't found** — Searching "Malaga", "Warnemunde" or "Tromso" returned nothing because the catalog stores accented names; the search now ignores diacritics. Added German exonyms (e.g. Tarent → Taranto). - **Cruise PDF/email import extracted nothing** — Real "Mein Schiff" bookings parsed to empty results because the LLM extraction prompt was over-constrained; the rewritten prompt reliably extracts ship, dates, cabin, price, itinerary ports and bundled fly & cruise flights. - **Port search failures were hidden** — A network or auth error during port search now surfaces an error instead of silently looking like "no results". ### Security - **API write-scope on cruise endpoints** — Cruise, port and ship write endpoints now require a write-scoped Personal Access Token (consistent with flights/trips), and the new port-lookup endpoint is rate-limited per user. ## [2.0.1] - 2026-06-13 ### Fixed - **Cruise parsing ignored the configured Ollama server.** The cruise booking parser always connected to `localhost:11434` instead of the Ollama URL/model set in admin settings, so a correctly configured — and successfully tested — remote Ollama was never actually used. Cruise PDF/email imports failed with an "LLM unreachable" error even though the connection test passed. The parser now resolves its endpoint from admin settings, matching the flight parser. - **Parser failures were logged as an empty object.** Backend error logs showed `error:{}` with no message or stack for parser errors (and 16 other call sites), hiding the root cause. Errors now serialize their message and stack, and the Ollama-unreachable error names the endpoint and points to Settings. ### Changed - **LLM parser configuration moved to the General admin tab.** The Ollama / parser settings sat under the "Flight" admin tab, where cruise-only users never found them; they now live under "General". ## [2.0.0-rc.12] - 2026-06-13 (Release Candidate) ### Fixed - **Cruise import preview triggered a render-loop.** The editable import preview reported its state through an effect whose dependencies changed on every render, causing a "Maximum update depth exceeded" loop (~193 console errors per import). Saving still worked but the screen thrashed; the reporting is now stable and the preview renders cleanly. - **Imported fly & cruise trip was mislabeled "completed".** A booking with bundled flights auto-creates a trip; with no explicit status it fell back to the "completed" default, so an upcoming voyage showed as already done. The status is now derived from the cruise dates (upcoming → planned, past → completed, in range → in progress). ## [2.0.0-rc.11] - 2026-06-13 (Release Candidate) ### Fixed - **Fly & cruise import failed at the final save step.** Importing a cruise with bundled flights created the cruise and the flights, but then errored while grouping them into a trip — the flight IDs were sent empty. The trip now links the cruise and its flights correctly. ## [2.0.0-rc.10] - 2026-06-13 (Release Candidate) ### Fixed - **Cruise import failed to save bookings that list only dates, no times.** Itinerary stop times extracted as a bare date (e.g. `2026-06-15T00:00`, without seconds or offset) were rejected by the cruise validation, so the whole import 400'd on save. Partial datetimes are now coerced to full ISO on the way in. ## [2.0.0-rc.9] - 2026-06-13 (Release Candidate) Cruise-import overhaul on top of rc.8. No schema change. ### Fixed - **Duplicate "Add cruise" button.** The dashboard cruise tab showed two "+ Kreuzfahrt hinzufügen" buttons; the correctly-placed toolbar button now opens the import chooser, and the stray floating one is gone. ### Added - **Editable cruise import preview.** After a booking is parsed, every field is editable inline before saving — the matched ship and ports now show by name, with an expandable itinerary editor, and missing/unmatched fields are flagged. (The preview was previously read-only.) - **Fly & cruise flights.** Cruise bookings that bundle flights now extract those flights too: they appear as opt-in cards with airports pre-filled (your home airport + the nearest airport to the embarkation/disembarkation port, all editable), and the cruise plus its flights can be grouped into one trip so the journey map shows fly → cruise → fly. ## [2.0.0-rc.8] - 2026-06-13 (Release Candidate) Bug fixes and import/UX improvements on top of rc.7. No schema change. ### Fixed - **Airport popups always showed "0 flights".** Clicking an airport on the map opened a card reporting 0 total flights and no routes, even though the map clearly drew arcs through it. The popup now aggregates the same flight data that draws the routes, so totals, top routes, airlines and distances match the map on every tab and honour the active year filter. - **Inconsistent CO₂ figures.** The per-flight CO₂ value, the dashboard "elephants" aggregate and the demo data each used a different formula, so the numbers disagreed. All three now use one model (distance band × cabin-class factor), and a one-shot backfill fills in CO₂ for older flights that never had it computed. ### Changed - **Trip cover image is now an upload.** The trip editor lets you upload an image file for the cover directly — including while creating a new trip — instead of pasting an image URL. - **Cruise add flow matches flights.** Adding a cruise now opens a single chooser like the flight form: import a booking (email or PDF, auto- detected) or enter it manually — on both the Cruises page and the dashboard cruise tab. ## [2.0.0-rc.7] - 2026-06-12 (Release Candidate) Closes the findings of the V2 function & usability audit. No schema change. ### Fixed - **Unreadable form fields on dark surfaces.** A Tailwind color named `base` shadowed the `text-base` font-size utility and rendered datetime inputs and dropdowns (e.g. in the cruise editor) near-black on the dark theme. - **Minimal cruises were invisible.** A cruise entered with only a departure and arrival port (no detailed stop list) drew no route on any map, listed neither port on its detail page, and reported contradictory port counts (4 in the list vs 0 on the detail page). Routes, distances, statistics and achievements now all run over the full departure → stops → arrival sequence, and changing the departure/arrival port recomputes the route. - **Past cruises stayed "Scheduled" forever.** Scheduled cruises whose end date passed more than 48 hours ago now flip to Completed automatically — cancelled and historical entries are never touched. - **"Go to settings" on a disabled dashboard tab led nowhere.** The link now lands directly on the Modules section. - **English leftovers in the German UI.** Loading, not-found and save-error messages, picker buttons and screen-reader labels across the cruise surfaces are now properly localized (DE/EN), and the logo badge reads V2.0 instead of V1.0. - **Parser failures exposed raw internals.** When the LLM parser (Ollama/OpenAI/Claude) is unreachable, the import dialog now shows a clear, actionable message instead of a connection stack trace — and the cruise statistics endpoint no longer fails entirely when a single cruise carries malformed data. ### Added - **German port names in the port search.** Typing "Lissabon", "Kopenhagen" or "Venedig" now finds Lisbon, Copenhagen and Venice — about 80 common German exonyms map to the English catalog names, matching even while you type. - **Cruise dashboard tab states.** The cruise map tab now shows a loading indicator, an error banner when data can't be fetched, and an empty-state card guiding new users to add or import their first cruise. The cruise editor labels its departure/arrival port fields, and search dropdowns no longer pop open on their own. ## [2.0.0-rc.6] - 2026-06-11 (Release Candidate) ### Added - **Trips are journeys now, not bookings.** Trip auto-detection only proposes real multi-leg journeys (3+ flights) — a plain out-and-back booking stays trip-less. A new "Clean up" tool on the Trips page dissolves legacy one-booking micro-trips (the flights are kept), and a merge mode combines several trips into one real journey. - **Compact trip filter in the flight list.** The one-chip-per-trip row (unusable with many trips) is replaced by quick chips for the newest trips plus a searchable, year-grouped "Filter by trip" popover. ### Fixed - **Disabled domains no longer leak content.** With the cruise domain switched off, cruises still appeared on the dashboard "All" map, in the cross-domain statistics, on trip detail pages and maps, and behind the dimmed cruise tab (including its controls). Disabled domains are now fully hidden — their data is not even fetched; trip details show a "N cruise segments hidden" hint and disabled dashboard tabs show an enable-in-settings stub instead. ## [2.0.0-rc.5] - 2026-06-10 (Release Candidate) Full V2 feature audit across both domains — closes a batch of usability gaps, mostly in the new cruise module. No schema change. ### Added - **Pick which trip the journey map shows.** Journey mode rendered an arbitrary first trip with no way to choose another; it now offers a trip selector (with a hint when no cross-domain trips exist). - **Fix unmatched ports during cruise import.** Ports the parser couldn't match were downgraded to sea days and could only be corrected after saving. The import preview now lets you pick the right port per stop before saving. - **Delete a cruise.** The cruise detail page now has a delete action (with confirmation) — previously a mis-imported cruise was stuck. ### Fixed - **The flight status filter hid historical and duplicated flights.** Unchecking any one status (flown/scheduled/cancelled) silently dropped every historical/duplicated flight; those two statuses now stay visible. - **Editing a flight could shift its stored time.** The edit modal showed times in the browser's timezone but saved them as airport-local, so opening and saving without changing anything drifted the departure / arrival when the two timezones differed. Times are now consistently airport-local and round-trip losslessly. - **The cruise stops editor showed blank ports when editing.** The selected port is now shown for each existing stop. - **Your own flight API key was labeled "Shared".** The provider card now reads the correct per-provider key status. - **Cruise import failures were opaque.** The real cause (e.g. Ollama not reachable) is now surfaced instead of a generic "parsing failed". - **Change-password accepted too-short passwords.** The modal now enforces the backend's 8-character minimum with specific error messages. - **English users saw stray German labels** (flight-row tooltips, scanner steps) and an unstyled duplicate-flight dialog — both fixed. ### Changed - **Cruise create opens straight to the form.** Removed the fake email-import chooser (a dead-end stub); real email/PDF import stays on the cruise list page. - **Removed the no-op "Require user flight API keys" admin toggle** — it had no backend behaviour. - The in-map visualization control is hidden in the airport-frequency and journey modes so it can't contradict the toolbar. ### Tooling - Dead-code cleanup (retired 3D map-mode layers + their i18n keys), corrected stale cruise-routing notes in CLAUDE.md, and routed the map's cruise-overlay gate through the domain-gating API. ## [2.0.0-rc.4] - 2026-06-10 (Release Candidate) ### Fixed - **The map could still drop a flight on very large accounts.** The rc.3 fix paginates the full flight set, but the backend ordered each page by `departureTime` alone — a column that is nullable and not unique. At a 500-flight page boundary, rows sharing a timestamp (or the cluster of flights with no timestamp at all) could be reordered between requests and silently skipped or drawn twice. Both the paginated flight list and the map's `/geo` feed now sort with a stable `id` tie-breaker, so every flight is loaded exactly once regardless of account size. ## [2.0.0-rc.3] - 2026-06-09 (Release Candidate) ### Fixed - **The dashboard map only showed your 100 most recent flights.** The multi-domain refactor replaced V1's paginated map load with a single unbounded `/geo` request, which the backend caps at 100 (newest first). On accounts with more than 100 flights, every older flight silently vanished from **all** map views (overview, heatmap, globe, journey) — a one-off trip from years ago simply wasn't there. The map now paginates the full set again, so every flight is plotted regardless of age. ### Changed - **Single-flown routes are easier to spot.** A route flown just once rendered at the faintest possible treatment (alpha 114, 1px) and was easy to lose in an empty ocean. Raised the visibility floor (alpha ≥ 160, minimum 2px width) so a lone long-haul reads clearly. ## [2.0.0-rc.2] - 2026-06-09 (Release Candidate) Second 2.0 release candidate. Data-integrity hardening uncovered while verifying real flight data on the rc.1 build, plus a trip-detection UX improvement. No schema change. ### Added - **Expandable trip-detection cards.** Each auto-detected trip in the "Reisen erkennen" review modal can be expanded to reveal its constituent legs (date · flight number · departure → arrival · status). The proposal payload now carries a `legs` array. ### Fixed - **Airport IATA collisions resolved to the wrong airport.** OurAirports assigns synthetic ICAO placeholders (e.g. `US-0226`) to minor airfields, some of which carry a spurious IATA that collides with a real international airport — so a flight to Antananarivo (IATA `TNR`) could be stamped with the coordinates of a tiny "Tulsa Downtown Airpark" and drawn to Oklahoma. Airport lookup now prefers the authoritative airport (active over closed, real 4-letter ICAO over a synthetic placeholder) across the cache, batch and create paths. ### Tooling - **`fixMistaggedDurations` maintenance script.** Detects flights whose legacy time-semantics tag inflates/deflates the displayed duration (e.g. a Sydney→Dubai leg showing 21 h instead of 14 h) by comparing the naïve and timezone-converted durations against the great-circle minimum flight time, and retags clearly mis-tagged rows. Genuinely corrupt rows (zero/negative duration) are reported for manual review, never silently changed. ## [2.0.0-rc.1] - 2026-06-09 (Release Candidate) First release candidate for **TravStats 2.0** — the multi-domain release. TravStats is no longer a flight-only logbook: it now tracks **cruises** as a first-class domain alongside flights, with a shared dashboard, shared stats, and a unified import pipeline. This RC aggregates the entire `2.0.0-beta.1` … `2.0.0-beta.28` line; see those entries below for the granular history. ### Added — Cruise domain - **Cruise logbook**: full CRUD for cruises, ships, and ports, with per-cruise itineraries (port calls + sea days). - **In-house sea-route renderer**: a vendored Eurostat _marnet_ shipping-lane graph with an A* pathfinder (`services/marnet/`) replaces the abandoned `searoute-ts` package, so cruise legs follow real shipping lanes instead of falling back to coarse arcs. - **Cruise booking parser**: AIDA / TUI / generic-LLM extraction wired into the email and PDF import routes, with ship/port entity resolution. - **Cruise stats**: a dedicated cruise stats page plus cross-domain KPIs (flights + cruises) on the combined "Alle" view. ### Added — Multi-domain platform - **Domain-scoped dashboard**: Alle / Flüge / Kreuzfahrten tabs with per-domain visualization modes; the URL carries tab + mode and the last-used mode persists per domain. - **Domain registry**: a shared front-/back-end domain registry drives gating, parsing targets, and per-domain brand palettes (dark-only). - **Special flights (Sonderflüge)** as a flight sub-type. - **Globe modernization**: MapLibre + deck.gl rewrite with GPU-based earth occlusion and smoothed map ↔ globe transitions. ### Fixed - **Flights to IATA-less airports no longer vanish from the map** (#120): airfields with only an ICAO code (common for small / pre-1990 airports) saved fine but were silently dropped by the route render gate. Airports are now identified by IATA → ICAO → coordinate key, so any leg with valid geometry renders. ## [2.0.0-beta.28] - 2026-05-12 (Beta) ### Fixed - **Infinite re-render loop on the dashboard fired ~8 GETs/sec at `/api/v1/flights`.** `useEnabledDomains()` returned a fresh `isEnabled` arrow function on every render; `DashboardPage` listed it in a `useEffect` dependency array, so the effect re-ran on every render, called `setCounts({...})` with a new object reference, triggered another render — and so on. With dev-tools open on the Globe view, the network log filled with thousands of 304-cached calls; with caching disabled this would hammer the backend. `MapContainer3D` already documented this trap and worked around it locally; the hook-level fix (memoize `isEnabled` against the `enabled` array via `useCallback`) closes the trap once and for all so future callers cannot trip into it. ## [2.0.0-beta.27] - 2026-05-10 (Beta) ### Fixed - **Bulk historical refresh aborted client-side after 10 s** while the backend kept running. The default axios timeout is 10 s, but `runBulkRefresh` loops sequentially through up to 25 flights, each calling AeroDataBox / Aviationstack — easily 30–60 s on a healthy link. The UI gave up mid-loop, surfaced a red `timeout of 10000ms exceeded` toast, and the user had to re-click before any data showed up. Bumped the bulk-refresh request to the parser-class 180 s timeout so the call stays open for the full server-side iteration. - **Settings → Auffrischen-Buttons used raw `bg-blue-600`** instead of the brand-amber `btn-primary`. Side-fix while in the file. ### Performance - **Globe hover tooltip moved into a leaf component.** Sweeping the cursor over airport / port labels triggered React re-renders at 60–120 Hz of the entire 1600-line `GlobeView` tree (every onHover event called `setTooltip`); on lower-end GPUs this read as visible jank during the cursor-following tooltip. The new `HoverTooltip` exposes an imperative `show / hide` API via `forwardRef`, so only the 30-line tooltip subtree re-renders on cursor movement — the parent and its `MapboxOverlay` setProps stay put. - **Aktivität-Panel rows replaced 720 `onMouseEnter` / `onMouseLeave` JS handlers** (2 per row × 354 rows + cruises) with a Tailwind `hover:bg-white/[0.04]` class. The browser now handles row hover purely in CSS — no listeners attached, no inline-style mutation during scroll. ## [2.0.0-beta.26] - 2026-05-10 (Beta) ### Fixed - **Sonder-Flug entry was invisible from the dashboard top-bar add button** — `DashboardLayout` mounted `SimplifiedFlightFormV2` without the `onPickSpecialFlight` prop, so the special-flight chooser card silently dropped out of the lookup step for every user coming through this path. The chooser button label was also calling a missing i18n key (`specialFlights:chooser.pickAction`); now uses the correct `chooser.cta`. - **NavigationBar missing on Trips pages** — `/trips` and `/trips/:id` had no top-bar at all (no logo, no nav links, no user menu) while every other top-level page mounted ``. Added it to both, plus the loading state on `/trips/:id` so the chrome doesn't pop in once the trip resolves. - **Map info pill bled through the dashboard's Aktivität toggle on the Alle and Flüge tabs** — `MapContainer3D` rendered a "X Flüge · Y Routen" pill at top-left z-10, sitting under the tab-level Aktivität button + domain-legend chips at z-30. The "Routen" tail peeked out between the two pills. Added a `hideInfoPill` prop and set it on AllTab + FlightsTab. - **Route-Details button on the flight tooltip silently did nothing** when the Aktivität sidebar was closed — `` is rendered inside ``, which only mounts while the sidebar is open. Clicking the button just flipped the store flag and looked unresponsive. FlightsTab now auto-opens the sidebar on any non-null `detailMode` transition so the details actually appear. ### Visual / brand - **Cruise-port markers + UN/LOCODE labels on the flat map**: hover tooltip ported from globe so airport / port hover bubbles now show IATA + name + count + last-visit / last-call across both surfaces. Labels also gain the same low-zoom hide threshold the airport IATA labels already use. - **Globe arc + cruise-path widths** realigned to the flat-map `sqrt(count)` formula so the visual jump between map ↔ globe transitions is smooth instead of jumping in line weight. - **Three-round brand-violation sweep** against `BRAND.md`: - Domain-color leaks fixed across AchievementPopup tier gradients, Admin/UserManagement role badge, DataSourceBadges, Stats seat-class and zone bars, Training flight-highlighter palette, FlightCalendar intensity ramp. - Email-import "Beste Option" hero card on the cruise side moved off the retired green/teal pattern to the same brand-amber gradient the flight side already uses; FlightLookupStep "Sonder-Flug" card border moved off Hotel-domain purple to flight-domain amber. - Stats progress bars (aircraft / airline / country) and the Setup + SetupPage success/error banners moved off light-mode Tailwind palette to brand state tokens (`--success` / `--danger` / `--warning` / `--accent`). - Admin LoggingManager + BackupManagement, Training TemplateReview - BoardingPassAnnotation, ParserPage tabs, ReceiptUpload drag- state and AirportSeedingBanner all rerouted to brand tokens. - Cruise port markers on the map now render in the canonical `--domain-cruise` hex (#6fa0d6) instead of sky-400. - **Shared tooltip labels** moved out of `map.globe.pinned.*` into a dedicated top-level `map.tooltip.*` namespace shared by both the globe pinned card and the flat-map hover bubble (no functional change, just consistent key location). - **Globe edge-clipping fixed** — `EarthOcclusionExtension` now reads the live MapLibre camera distance off the deck.gl viewport instead of an empirical zoom heuristic that was off by 3-22× from the real camera. Routes no longer disappear at the rim when zoomed out. ### Fixed - **Alle-tab map legend now reflects domain pills** — The legend stripe (sky-blue cruise + orange flight) hard-coded both rows, so toggling cruise off in the filter dropdown left the legend lying that cruises were still on screen. Each row is now gated on its domain pill state; if both pills are off, the legend block is hidden entirely. ## [2.0.0-beta.24] - 2026-05-09 (Beta) ### Added - **Filter button shows active state** — When the year dropdown picks a specific year or any domain pill is off, the dashboard top-bar Filter button switches to an accent border + the picked year next to the label, and a "n/m" pill displays how many domains are active. Without this indicator users can't tell at a glance whether they're seeing the full dataset or a filtered slice. - **"Filter zurücksetzen" link in the dropdown** — Single click to clear year + domain selection back to the unfiltered defaults. Disabled when no filter is active. ## [2.0.0-beta.23] - 2026-05-09 (Beta) ### Fixed - **Double tooltip on flat-map airport markers** — Beta.21 added a hover bubble via deck.gl's `getTooltip` to surface full place names on every marker. On the flat map, airport markers already trigger the rich `AirportTooltip` (departures/arrivals/distance/route counts) on click, so the new hover bubble layered a second tooltip on top of the rich one. The hover bubble now skips flat-map airports (`routes-dot` / `routes-label`) and fires only on flat-map ports + every globe marker. Globe airports keep the hover bubble because they have no rich-tooltip equivalent. - **Markers clipped where cruise paths cross them** — Globe airport + port markers rendered at altitude 0 while cruise paths sit at 5 km; the depth buffer let the path's fragments occlude the marker dot at shallow camera angles. Lifted both the dot and the label to 8 km altitude (`MARKER_ALTITUDE_M`) so the marker is reliably above any path that crosses it. On the flat map the symptom was different: cruise paths were composed AFTER airport visuals in the layer stack, so they drew on top of airport dots at every intersection. Reordered the flat-map layer composition: cruise arcs/arrows go below flight arcs and airport markers, cruise port halo+dot+label go on top of everything (their pixel-radius cap keeps them readable at every zoom). ## [2.0.0-beta.22] - 2026-05-09 (Beta) ### Fixed - **Year + domain filter now actually filters data** — Beta.20 added the year dropdown and domain pill row but only updated the store; the data layer didn't read either. Wired both into the Alle, Flüge, and Kreuzfahrten tabs: - Alle tab filters flights by `departureTime` within the year range and cruises by interval overlap (start/end), and gates each domain on/off via the pill row. Activity panel reflects the same filtered set. - Flüge / Kreuzfahrten apply the year filter only — domain pills are cross-domain and a domain-dedicated tab keeps showing its domain regardless of pill state. - `MapContainer3D` gained a `cruisesOverride` prop that bypasses its internal cruise fetch so the tabs can hand it the pre-filtered set. ## [2.0.0-beta.21] - 2026-05-09 (Beta) ### Changed - **Flat-map cruise port labels show UN/LOCODE instead of full name** — Mirrors how airport markers show 3-letter IATA codes ("HAM", "JFK"). Ports now show the 5-letter UN/LOCODE ("DEHAM", "USJFK") with a fallback to the full name when the UN/LOCODE column is null. The Globe view already did this — flat-map was inconsistent. ### Added - **Hover tooltip on every airport + port marker** — Shows the full place name in a small dark bubble on hover. Wired through `getTooltip` on every MapboxOverlay (DeckGLMap, GlobeView, CruiseRouteMap, TripMap) using a shared `components/map/markerTooltip.ts` helper. Routes / arcs / paths are explicitly excluded so the cursor stays clean while panning. ## [2.0.0-beta.20] - 2026-05-09 (Beta) ### Changed - **Globe mode is now exclusive to the Alle tab** — The 3-D globe is cross-domain by design (mixes flights and cruises on one sphere), so exposing it on per-domain tabs caused two problems: clicking it on Flüge/Kreuzfahrten swapped to a globe view that broke domain scoping, and the heatmap legend never matched. The in-map FAB now takes an `availableModes` subset — `[routes, heatmap, globe]` on Alle, `[routes, heatmap, trips]` on Flüge, and the FAB is hidden entirely on Kreuzfahrten (cruise modes don't map 1:1 to the FAB's Map modes). - **Flat-map cruise port markers re-aligned with airport markers** — Ports were a single 4–10 px scatter dot scaled by visit count, while airports are a meter-based dot + halo ring stack. On the same map they read as two unrelated marker systems. The port layer now mirrors the airport stack: 2.2 km solid dot in sky-blue + 6 km halo ring + clickable port-name label (TextLayer, billboarded). Same visual weight at every zoom. - **Empty ship-marker toggle removed** — The "Schiffsmarker" checkbox in the globe FAB row had no real handler — toggling it left the layer state untouched. Removed the toggle and the dormant shipMarkers state plumbing in GlobeView and buildGlobeLayers. ### Added - **Year + domain dropdown filter** — The dashboard filter dropdown now offers a single year selector (current year + 14 years back, or "Alle Jahre") instead of two from/to date pickers. Per-day precision is unnecessary for the use cases TravStats supports, and consolidates against the Globe day slider so the two don't fight. On the Alle tab a domain pill row toggles flight / cruise / poi visibility together. Store extended with `setYear` (mirrors to the underlying `time` range so existing range-overlap consumers keep working) and `setDomains`. ## [2.0.0-beta.19] - 2026-05-09 (Beta) ### Fixed - **Cruise paths clipped into the globe** — Cruise sea-route geometry comes from the backend as 2-D `[lng, lat]` points, so deck.gl rendered the path at exactly altitude 0. That shares depth-buffer values with the sphere mesh and produces visible z-fighting / clipping where segments dip into the globe. Lifted each path point to 5 km altitude in `globe-cruise-paths`'s `getPath` — invisible at any user-relevant zoom but well above the depth precision noise. ## [2.0.0-beta.18] - 2026-05-09 (Beta) ### Fixed - **Globe pinned-card "Open last flight" CTA was a no-op** — The CTA was wired to a callback named `onFlightClick` that mapped to `setSelection([flight])` upstream, which only highlights the row in the activity panel. Renamed to `onFlightOpen` and re-wired in AllTab + FlightsTab to actually open the flight edit modal — same modal the activity-panel "Details" button opens. ### Added - **Cruise pinned-card now has an "Open cruise" CTA** — Symmetric to the flight CTA. New `onCruiseOpen` prop on GlobeView / MapContainer3D, wired in AllTab to navigate to `/cruises/:id` — same destination the activity-panel cruise "Details" button uses. ## [2.0.0-beta.17] - 2026-05-09 (Beta) ### Fixed - **Globe canvas still disappeared on click after beta.16** — Removing `flyToArc` and adding try/catch wasn't enough. Per Gemini's diagnosis (matching upstream issue #512), the actual root cause is MapLibre 5's `locationOccludedOpacity` feature: it performs an internal occlusion pass that does not restore `gl.SCISSOR_TEST`, leaving the shared GL context corrupted on next deck.gl draw. With `interleaved: true` in `MapboxOverlay`, deck.gl and MapLibre share the WebGL context, so any popup creation triggered a state leak that blanked the canvas. - **Replaced `maplibregl.Popup` with a custom React overlay** — Pinned-card now mounts as an absolutely-positioned `
` whose `left/top` come from `map.project([lng, lat])`, refreshed on every MapLibre `render` event. Back-of-globe culling is replicated in JS via the same dot-product math the EarthOcclusionExtension shader uses. Same UX (anchored, follows camera, fades when occluded), zero GL-state risk. - **`PinnedCardBoundary` error boundary around the card** — Defense in depth: a throw inside the React subtree now logs and renders null, instead of bubbling up and unmounting GlobeView. ## [2.0.0-beta.16] - 2026-05-09 (Beta) ### Fixed - **Globe disappeared completely on flight-route click** — Critical regression in beta.12-15: clicking an aggregated arc fired both `setPinned()` (mounting the new MapLibre Popup) and a 1.5 s `flyTo()` camera animation. The popup mount + camera flyTo + globe re-render combo crashed the WebGL canvas in some camera states, leaving a black screen instead of the globe. Two-layer fix: - **Drop the camera flyTo on click.** With the popup now anchored to the click coordinate, flying the camera away from where the user just tapped is anti-pattern anyway, and removing it eliminates the race entirely. - **Wrap the popup mount in try/catch + finite-coord guard.** A single throw from MapLibre's projection or anchor-flipping math used to bubble up and unmount the entire GlobeView. Now caught and logged; popup missing is recoverable, canvas going away is not. ## [2.0.0-beta.15] - 2026-05-09 (Beta) ### Changed - **Inactive-tab / section-nav contrast bumped consistently** — The TripDetailPage tab fix in beta.10 used a one-off `rgba(230,237,243,0.65)` to lift inactive tabs above the borderline WCAG-AA contrast `--text-muted` (#8b949e) was hitting against `--bg-base`. That fix is now a proper design token, `--text-secondary` (#a8b3bf), applied everywhere the same pattern was leaking through: AdvancedStatsPage tab strip (Alle / Flug / Kreuzfahrt filter), AdminPage tabs + section sidebar, SettingsPage section sidebar, and TripModal sub-tabs. `--text-muted` itself is unchanged and stays reserved for genuinely de-emphasised content (captions, scrollbars, borders). ## [2.0.0-beta.14] - 2026-05-09 (Beta) ### Fixed - **Globe pinned-card width stretched to viewport on long content** — The MapLibre Popup wrapper was created with `maxWidth: "none"`, which let the inner card balloon out to the full viewport width as soon as the content didn't naturally cap itself. Capped at `320px` so the inner card's own min/max widths drive the layout. - **Cruise card duplicated the port / sea-day count** — Both fields used pluralised i18n keys that already include the count (`{{count}} Häfen`), and then rendered the count again as the value cell, producing rows like `6 HÄFEN 6`. Replaced the pluralised keys with plain `Häfen` / `Seetage` labels. - **Port region rendered lowercase** — The `port.region` column in the seed (e.g. `mediterranean`) is the raw enum value. Capitalised for display. ## [2.0.0-beta.13] - 2026-05-09 (Beta) ### Added - **Globe pinned-card content density (Phase B)** — The pinned detail card that opens on click now follows a 3-tier hierarchy: heading (icon + identifier), hero stat (the one number you want to see), and a metadata grid of 3-5 high-value facts. Concrete fields: - **Airport**: visit count + name + longest route from here + top airline + top aircraft + last visit date. - **Port**: visit count + country + region + ships visited (truncated to 3) + longest port-call duration + last call. - **Flight route**: times flown + total kilometres (sum across all aggregated flights on this route) + last flight date + aircraft types + top airline + the existing "Open last flight" CTA. - **Cruise**: cruise line + date range + port count + sea-day count - embark / debark ports. All aggregations are pure helpers in `Globe/cardStats.ts` derived from the `flights` / `cruises` arrays already passed to GlobeView — no extra API roundtrip. Card content extracted into `Globe/PinnedCard.tsx` (~330 LoC) so the GlobeView shell stays focused on map + popup lifecycle. 21 new i18n keys per locale. ## [2.0.0-beta.12] - 2026-05-09 (Beta) ### Changed - **Globe pinned-card now anchors to the clicked feature** — The detail card that opens when you click an airport, port, route arc, or cruise path used to live in a fixed bottom-right slot, disconnected from the feature it described. It now mounts inside a MapLibre `Popup` bound to the lng/lat where the click landed (`PickingInfo.coordinate` for arcs and cruise paths, `position` for airport/port markers), so the card visually belongs to the feature with a tail pointing at it. MapLibre handles edge-aware anchor flipping near the viewport borders, and `locationOccludedOpacity:0` uses the renderer's own globe-visibility math to fade the card when the anchor rotates to the back of the earth. The card content itself is unchanged in this beta — Phase B (denser content per Gemini + Codex review) lands in the next bump. ## [2.0.0-beta.11] - 2026-05-09 (Beta) ### Fixed - **Activity-toggle and legend overlapped on the dashboard `Alle` tab** — The `☰ Aktivität` button and the flight/cruise legend were two independent absolutely-positioned pills with hand-tuned offsets (toggle at `left:12`, legend at `left:128`). Tight gap and variable letter widths caused the legend pill to render on top of the toggle's right edge. Both pills now share a single absolute flex container with `gap:8`, so they auto-flow without offset math; the container as a whole shifts to `left:340` when the sidebar opens. ## [2.0.0-beta.10] - 2026-05-09 (Beta) ### Added - **Trip System V2** — A redesigned Trip surface with a dedicated detail page carrying five sub-tabs (Overview / Timeline / Map / Gallery / Logistics). Iterations 1-9 land in this beta: list-card redesign, stops + journal entries, real Map tab with click-to-fly and Globe/Flat projection toggle, per-proposal review flow for trip auto-detection, TripModal split into 4 sub-tabs with cover preview, photo gallery with lightbox + upload, and LLM-generated trip summaries via Ollama (gemma3:12b, 3-paragraph DE travel-blog format, persisted to `trip.summary`). - **Special Flights as a Flight subtype** — Sightseeing, repositioning, training, and charter flights can now be logged with type-specific metadata (event location, label, lat/lon) without distorting the Flight schema or the Trip system. Special-flight overlays render on top of routes-mode with their own legend and tooltip variant — they are NOT a separate domain or MapMode. - **Globe modernization** — The 3D Globe is rewritten on MapLibre 5 + deck.gl `MapboxOverlay` + `useControl`, replacing the legacy `react-globe.gl` + Three.js engine. Earth-occlusion is now GPU per-fragment via a custom deck.gl extension reading camera state every frame. New helper modules (`buildGlobeLayers`, `arcUtils`, `heatmapUtils`, `globeLayerTypes`) replace inline implementations. - **Merged 1.5.0 trunk** — beta.10 carries the full 1.5 main payload: FR24 CSV importer, generic CSV importer with column-mapping wizard, shared import preview modal, AeroDataBox extended-field capture, dataSource badges, historical-flight Day select, `POST /import/parse` API endpoint, and the `route_distance` backfill script. ### Changed - **Settings → Import promoted to top-level (General tab)** — The 1.5.0 import section landed nested under Settings → Flug → Import, one tab-level deep. With V2 multi-domain ahead (cruise / hotel / POI imports) and feedback that the nesting hid the section, Import now lives in the General tab right after Backup. The three tiles (Flightradar24 CSV, generic CSV with mapping wizard, Round-Trip XLSX) and all underlying parsers are unchanged. - **Globe default basemap** — From `standard` (openfreemap liberty) to `dark` (CartoDB dark-matter), matching BRAND.md §1.1's dark-only app shell. Users can still pick any style; the choice persists in `sessionStorage`. - **TripDetailPage TabBar inactive contrast** — Inactive tab labels brightened from `--text-muted` to white at 65 % opacity with a hover transition to `--text-primary`. - **Globe perf** — Texture sharpness, live-mode smoothness, and removal of the night cycle. Frame pacing is now fully GPU-driven. ### Fixed - **Trip delete-confirmation heading was nearly invisible** — The `

` in the delete-confirm modal on the Trip detail page was missing an explicit text colour and inherited a muted shade against the dark surface. Resolved by adding `style={{ color: "var(--text-primary)" }}` in line with the existing TripsTab and FlightPanel patterns. - **`TEST_LEAK_*` regression-test achievements bled into the user UI** — The `achievements.scheduledLeak.test.ts` regression suite seeds nine `TEST_LEAK_*` rows into the achievements table to verify scheduled-flight isolation, but the seeds remained after the test ran and showed up in the live Achievements page (e.g. "Leak: First Aircraft Type"). The list / recent / leaderboard endpoints now filter `code NOT LIKE 'TEST_%'`, and the dev DB has been cleaned. - **deck.gl warning `fontSettings.sdf is required to render outline`** — The IATA label TextLayer set `outlineWidth: 2` + `outlineColor` but never enabled the SDF font flag, so the outline never actually rendered and deck.gl logged a warning every frame. `fontSettings: { sdf: true }` is now set explicitly. ### Database - **Four new migrations** — `20260509120000_trip_metadata`, `20260509130000_trip_stops_and_journal`, `20260509140000_trip_photos`, `20260509150000_special_flights_subtype`. All additive, idempotent, zero-downtime safe. Plus the 1.5.0 `_add_aerodatabox_extended_fields` migration carried in via the main merge. ## [2.0.0-beta.9] - 2026-05-05 (Beta) ### Added - **Brand token system wired through the app** — `BRAND.md` per-domain palette (flight `#f0a947`, cruise `#6fa0d6`, hotel `#b072d6`, poi `#5ec2b2`) is now the single source of truth. The shared domain registry (`shared/domains.ts` on both backend and frontend) carries the canonical hexes, so dashboard tab strip, AllTab legend, journey layers, and the legend-driven map controls all read from one place. Stats screen consolidated onto a shared `StatCard` surface — ~30 arbitrary Tailwind gradients replaced with brand tokens. - **Merged 1.4.0 trunk** — beta.9 carries the full 1.4 main payload: AeroDataBox provider + bulk historical refresh, persisted tail number / Mode-S / codeshare metadata, Aircraft hulls ranking + per-tail profile pages (`/aircraft/:reg`), API tokens for headless ingestion, OpenAPI spec at `/api/v1/openapi.json` + Swagger UI at `/api/v1/docs`, demo account flag (`isDemo`), upgrade-backup hook on container entrypoint. ### Changed - **Dark-only theme**, light-mode classes retired across components. `themeStore` simplified to `mapTheme` only — no more `isDarkMode` toggle. `` is set defensively at boot so a stale `localStorage` payload can't strip it. ## [1.5.1] - 2026-05-12 ### Fixed - **`backfillRouteDistance.ts` now actually runs in production** — The v1.5.0 CHANGELOG advertised the script as runnable via `docker exec TravStats npx tsx /app/backend/scripts/backfillRouteDistance.ts`, but the production Dockerfile only copied `backend/dist/` out of the builder stage so `/app/backend/scripts/` did not exist in the shipped image and every operator who followed the documented path got "No such file or directory". The Dockerfile now explicitly copies `backend/scripts/` into the production stage; the documented command resolves as promised. - **AeroDataBox lookup now persists airport `shortName` and `municipalityName`** — The v1.5.0 release notes claimed AeroDataBox lookups populate these airport-side columns, but the adapter's airport-object interface declared only `{iata, icao, name}` and the fields were filled only by the airport seeder on first boot — provider responses never reached them. New `enrichAirportMetadata()` helper backfills NULL slots from AeroDataBox responses on every lookup, never overwrites a curated value, invalidates the airport cache after a write. Six new lock-in tests cover the contract: happy-path, never-overwrite-curated, both-already-set no-op, empty-payload no-op, unknown-code, and whitespace trimming. ### Tests - Backend Jest: 657 → 663 (+6 for `enrichAirportMetadata`). Frontend Vitest unchanged at 357/357. ## [1.5.0] - 2026-05-08 ### Added - **`GET /api/v1/diagnostics`** — User-scoped, redacted JSON snapshot of selected flights + trips for bug reports. Filters: `flightIds`, `tripIds`, `airline`, `since`. Reduces the manual hand-glueing maintainers used to need (manual `?airline=…` query + JSON trim + redact). Closes #105. Distinct from the existing `/diagnostic-export` (which returns server log tails). `recentErrors` is reserved as `[]` for now — error-events table is v1.5.x scope. Documented in the OpenAPI spec under the `Diagnostics` tag (rc.5). - **FR24 CSV importer** — Settings → Import → "From Flightradar24" takes the unmodified `my.flightradar24.com` CSV export. Server-side timezone conversion via the existing airport lookup, hardcoded column map for FR24's known schema (handles the leading blank line and embedded `(IATA/ICAO)` annotations), `Flight reason → category`, `Seat number → seatNumber`, `Note → notes`. Status auto-defaults from `dep_utc` vs. now (past → `flown`, future → `scheduled`). Closes #99. - **Generic CSV importer with column-mapping wizard** — Drop any flat per-flight CSV; a wizard opens automatically and lets you assign each column to a TravStats field (Departure IATA, Departure Time, Arrival IATA, Status, Airline, Aircraft, …) with auto-suggestions for known field names. - **Re-import TravStats Excel relocated to Settings → Import** — The round-trip XLSX/CSV/JSON importer (existing rows updated by ID, new rows created) moved off the dashboard into the new dedicated section so all three importers share one place. - **Shared import preview modal** — Every importer routes through the same preview screen: per-row checkboxes, flag badges (`duplicate`, `duration_mismatch`, `unresolvable_airport`, `date_invalid`), commit dispatched in 20-flight chunks via the existing `/flights/batch` route. Each row tagged with the appropriate `dataSource` (`imported_fr24`, `imported_generic_csv`, `imported_roundtrip`). - **AeroDataBox extended-field capture** — Lookups now persist `runwayDepartureTime`, `runwayArrivalTime`, `isCargo`, `aerodataboxQualityTags`, `baggageBelt`, `checkInDesk`, plus airport `shortName` and `municipalityName`. Backfill onto existing rows runs through the existing scheduler. - **DataSource badges for imports** — Flight cards now show `📊 imported_fr24` (orange), `📥 imported_generic_csv`, `↻ imported_roundtrip` so the import provenance is visible at a glance, distinct from `📧 email_import` and `🎫 boarding_pass_scan`. - **Historical-flight Day select** — The historical-mode date input gained a Day dropdown (Year + Month + Day) so flights with a remembered exact date but no precise time can store as `YYYY-MM-DD` with `depTimeSemantics: 'DATE_ONLY'`. Year-only and Year+Month entry continue to work unchanged. - **Year-inferred warning badge** — BCBP boarding-pass barcodes carry only a Julian day-of-year (no year ever) and the Tesseract OCR fallback silently substitutes the current year when none is captured. Both paths now mark `departureTime` as inferred so `FlightReviewModal` shows the existing yellow "!" badge with a "Year guessed — please verify" tooltip. - **Historical flights now feed time-insensitive stats** — Old logbook entries (status `historical`) contribute to airport count, country count, continent count, distance, route counts, seat-class breakdown, and milestone-year — every stat that depends only on date / airport / airline. Time-of-day buckets, layovers, durations, fastest-route km/h, and midnight crossings stay `flown`-only because historical timestamps carry placeholder times that would skew them. - **`POST /api/v1/import/parse` API endpoint** — External tools and Personal-Access-Token consumers can now run the full FR24 / Generic-CSV import flow over the API: parse → preview → batch-commit. Body is `{source: "fr24", csv}` or `{source: "generic_csv", csv, mapping}`; returns the same `PreviewRowInput[]` shape the browser produces. Read-scope tokens are sufficient (the parser is a pure function, no DB writes). External scripts no longer need to reimplement the RFC-4180 lexer or FR24-specific column mapping. - **Generic-CSV wizard auto-detection** — Headers matching common aliases (`Flight Date` → date, `Origin`/`dep`/`from` → fromIata, `Destination`/`arr`/`to` → toIata, `Tail`/`reg` → registration, `carrier` → airline, `dpt_local`/`arr_local` → dep/arr-time, …) are pre-mapped on upload, with a green "Auto-erkannt" badge so the user knows what to double-check. - **Inline CSV sample preview in the wizard** — Each dropdown option shows `header_name (Sample: row-1 value)` plus a muted preview line below selected dropdowns, so cryptic headers like `dpt_local` or `ac_reg` are immediately readable without opening the source file. - **`backend/scripts/backfillRouteDistance.ts` script** — One-shot Node script that walks all flights with `route_distance NULL` + non-NULL coords, computes Haversine, UPDATEs in batches of 500. Idempotent (`where: { routeDistance: null }`), supports `--dry-run` and `--batch-size` flags. Run via `docker exec TravStats npx tsx /app/backend/scripts/backfillRouteDistance.ts` to backfill legacy NULL distances on any host that imported flight data before v1.5.0-rc.3. Norberts box would populate ~231 NULLs in well under a second. ### Changed - **Routes-layer rendering** — Maximum line width reduced from 7 px to 4 px so dense maps stay readable. Routes carrying only future scheduled flights render as solid sky-blue arcs (no gradient). Routes with both past and upcoming flights render with a hardcoded two-tier red core (red-400 below median frequency, red-600 at/above) plus blue tips, replacing the heatmap-tinted core. Routes with only flown legs continue to use the frequency-driven heatmap palette. - **Settings → Import UI** — Redesigned to match the rest of the Settings design system: outer `SectionCard` with title, three sub-cards in a responsive grid (1 / 2 / 3 columns by viewport), `btn-primary` file pickers with 📁 prefix replacing native `` buttons, red-tinted error blocks, green-tinted success blocks. - **Generic CSV import description** — Reworded so the upload-then-wizard flow is explicit ("Upload a flat per-flight CSV — after upload, a mapping wizard opens so you can match each column to a TravStats field"), avoiding the prior implication that "Wizard" was a button. ### Fixed - **`durationMinutes` returned 0 / negative numbers on DATE_ONLY rows** — Manually entered round-trips between airports in different timezones (e.g. RAK↔MAD with both sides marked DATE_ONLY) produced -120 / 0 / +120 minute durations because the 12:00 wall-clock placeholder was put through real airport-timezone conversion. The `tzAwareDurationMinutes` type union didn't even include `DATE_ONLY`; a silent cast in the flights list endpoint mapped it to `UNKNOWN` and ran a naive UTC diff. Now returns `null` when either side is `DATE_ONLY`, and the display layer falls back to the great-circle estimate (Iberia RAK→MAD now reads "~1h 35min" instead of "0min" or "-2h"). Closes #106A; closes #103 as a side-effect (PUT was failing on the resulting bad rows). - **Free-text airline names not resolved to IATA/ICAO** — Manual entries like `"Iberia"` saved with `airlineIata=null` / `airlineIcao=null`, so airline filters, codeshare detection, and stats grouping treated spelling variants as separate carriers. New `resolveAirlineCodes()` does strict-exact lookup (canonical name, IATA code, ICAO code, or explicit alias — no fuzzy substring matching, since "Air India" → "Indian Airlines" is worse than `null`). Wired into `POST /flights`, `PUT /flights/:id`, and `POST /flights/batch` so all importers + manual entry benefit. `airlines.ts` gains an `icao` field on every entry. Closes #106B. - **Trip auto-assignment skipping the negative-duration leg** — The RAK→MAD return that ended up with `durationMinutes=-120` (cause above) was silently filtered out of trip association on the frontend. Closes #104 as a side-effect of the duration fix. - **Chronological-order refine now date-granular for DATE_ONLY** — The RC3 `arrivalLocal must not precede departureLocal` check was triggering on same-day DATE_ONLY round-trips because the 12:00 placeholder shifted by airport timezone produced arrUTC < depUTC. Now compares only the date portion when either side is `DATE_ONLY`; cross-day mismatches still rejected. - **Two-stage-review quality findings on import code** — Several issues caught by the post-implementation review: silent chunk-error swallowing in commit dispatcher (now surfaced), hardcoded English in mapping-wizard labels (now fully i18n'd DE+EN), missing parse-error truncation hint, `useState(initialChecked)` stale-state risk in `PreviewModal`, missing `aria-labelledby` on the modal. - **`imported_fr24` badge collided with `historical_enrichment` colour** — Both rendered as amber on the same flight card when an FR24-imported flight got enriched. `imported_fr24` now uses orange; the others stay amber. - **API silently accepted historical flights with `arr_time` before `dep_time`** — The `createFlightSchema` chronological-order refine previously skipped `historical` and `duplicated` statuses, so an overnight flight without day-rollover (Norberts ATL→MUC 1986 case, arrival 8 h before departure) passed through `POST /flights` and `POST /flights/batch` silently. The refine now applies to every status when both times are set; NULL `arrival_time` remains legal for historicals so legitimate date-only bulk imports keep working. Surfaced by Norberts UAT on his Unraid-2 instance. - **API silently accepted historical / flown flights with future dates** — No year-axis sanity check; future-dated `historical` rows (Norbert had three 2026-dated EI-flight rows from a year-typo at import time) and future-dated `flown` rows were both accepted. Schema now rejects them. `scheduled` with a past date is intentionally NOT a hard reject — the create handlers log `flight_create_scheduled_in_past` for ops review instead, since legitimate edge cases (just-departed manually re-edited rows) exist. - **`PUT /flights/:id` had no chronological refine at all** — `updateFlightSchema` was missing both the chronological-order and year-axis refines that `createFlightSchema` had since the start. Updates that flipped `arr` before `dep`, or that retroactively re-stamped a historical to the future, all passed silently. The same shared refines now also gate every update payload. - **`route_distance` was never written by the regular insert paths** — Only Provider lookups (AeroDataBox, AirLabs) populated it; `POST /flights`, `POST /flights/batch`, and `PUT /flights/:id` left it NULL. Result: total km, longest flight, and the distance-tier achievements were blank for every user who hadn't explicitly run a Provider refresh — including all 231 of Norberts flights. Haversine is now computed from the enriched dep/arr coords on every insert and recomputed on `PUT` whenever a coord changes. Same source of truth as `co2Kg`, which had been computing Haversine internally for CO2 but not persisting the distance. - **Import preview modal redesign** — Flightradar24's CSV exporter uses UTC+4 instead of `Asia/Kolkata` (+5:30) for Bengaluru, so BLR↔non-India flights tripped `duration_mismatch` and got their checkbox disabled by the import preview. The flag is now treated as a soft warning: the row stays selectable and renders the backend-calculated Date/Status normally, with an explicit "Duration mismatch — using calculated arrival from departure + duration" badge. Reported by jay-tau in issue #99 UAT. - **Import preview modal redesign** — Cancel and Import buttons no longer concatenate as "CancelImport 25 rows"; the modal now uses the same centred-overlay / rounded-card / flex-footer pattern as `DiagnosticExportModal`. After a successful commit it transitions to a "✅ Imported N flights" success state with a link to the flights list, instead of silently closing (which read like the import had failed when the user re-opened it and saw the just-imported rows flagged as `exact_match` duplicates). - **Generic-CSV wizard UX** — Persistent status footer replaces the silently-disabled Continue button: "Pflichtfelder fehlen: Datum, Von IATA" in red when blocked, "Alle Pflichtfelder zugeordnet · 3 optional übersprungen" in green when ready. Required and Optional fields are grouped into separate sections. Collision detection: the same CSV column mapped to two TravStats fields turns both dropdowns red and blocks Continue until resolved. - **i18n plural keys for the import preview** — `commit` / `imported` / `failed` migrated from the legacy `_plural` suffix (broken in i18next v23) to `_one` / `_other`, so "Import 2 row" / "✅ 2 Flug importiert" pluralises correctly. - **FR24 import: arrival time stored 1h30 off on BLR-international (rc.6)** — RC.2 made `duration_mismatch` rows selectable but stored `arr_utc = dep_utc + (broken) Duration`, inheriting FR24's wrong Bengaluru offset (UTC+4 instead of `Asia/Kolkata` +5:30) on the very rows the flag was meant to warn about. RC.6 anchors `arr_utc` on `arr_local + arr_tz` — the authoritative signal — and uses Duration only to disambiguate the calendar day on trans-meridian flights via a best-day-shift search (0/+1/+2/+3 days from dep date). The `duration_mismatch` flag still fires for transparency when even the best day-shifted candidate is more than 30 min off the duration-derived target. Reported by jay-tau in issue #99 RC.2 UAT (BLR→LHR / BA119 / AF191 round-trip showed -1h30 / +1h30 / -1h30 drift). The trans-meridian LAX→SYD test case had hardcoded `delta = 14h55`, which embedded FR24's stored Duration; the real wall-clock flight time is 13h55 (LAX PDT → SYD AEDT, both in DST on 2023-03-22) and the test was updated to match the correct semantics. - **Sticky preview-table header bled through scrolling rows (rc.6)** — `` in `PreviewModal` lacked a stacking context, so rows with rgba warning/error backgrounds painted over the column headers when the preview overflowed. Added `z-10` to the sticky thead. - **Trip auto-detection orphaned DATE_ONLY return legs (rc.7)** — `detectTrips` reads flights ordered by `departureTime asc`. For manually entered DATE_ONLY round-trips, default timestamps don't reflect within-day chain order — Norberts 2009-09-21 return-day stored RAK→MAD with `dep=12:00 UTC` and the connecting MAD→MUC anchor with `dep=10:00 UTC`, so a raw-timestamp sort placed the anchor BEFORE the return leg. The home-loop walker then closed the loop early and orphaned the return (issue #104). The earlier `durationMinutes` fix (#106A, rc.4) cleaned up presentation but didn't change DB sort order, so the RC.4 changelog entry crediting this as a side-effect of #106A was premature. RC.7 adds `chainCoherentSort`: within each calendar date, Kahn's topological sort using arr→dep airport matches as chain edges, with `departureTime` as a stable tiebreak between unrelated same-day chains. Single-flight days and already-timestamp-correct days are unaffected; multi-day order is preserved. The home-loop and continuity heuristics now see Norberts MUC↺RAK return leg in the right place and capture all 4 flights. ### Tests - **E2E Playwright critical paths for all three importers** — End-to-end coverage for FR24, Generic-CSV (full mapping-wizard flow), and Round-Trip imports against a fresh dev DB. 5 new tests, suite now 90 across 6 files. - **Regression tests for scheduled-flight leakage** — Locked-in coverage that scheduled flights cannot trigger general achievements (`flights_count`, `airports`, `countries`, `continents`, …) or contaminate `highestAirport` / similar unique stats. The engine architecture filters at the SQL layer; these tests prevent silent regressions. - **Bug-slot unit tests** — 32 new tests for the historical-date shape discriminator (`useFlightForm.dateShape.test.ts` covering 4 storage shapes), 7 for `bcbpToScanResult`, 4 for Tesseract `extractDateTimes`, plus 10 for historical-flight stats inclusion. Backend 607/607, frontend 348/348. - **RC2 regression coverage** — 6 new wizard tests (auto-mapping exact + alias, missing-required hint, collision detection, sample rendering, cancel), 3 preview-modal tests (soft-warning row stays importable, success-state transition, error-state recovery), 1 backend lock-in test for the `duration_mismatch` BLR-route contract using jay-tau's exact numbers, and 11 backend tests for the new `/import/parse` route + parsers (parser unit tests + supertest integration: 401 unauth, 200 happy paths for both sources, 400 malformed body, 400 oversized body). Backend 619/619, frontend 357/357. - **RC3 API-hardening regression coverage** — 5 schema tests (G1 chronological order for historicals, G2/G3 year-axis sanity for historical/flown, G5 same refines on update payloads), 3 backfill-script tests (idempotency, NULL-coord skip, Haversine sanity for short/long-haul/trans-Tasman), plus 5 implicit deltas in pre-existing flight-route tests. Backend 632/632. - **RC4 data-integrity coverage** — 13 new tests (`dataIntegrity.rc4.test.ts`) covering the `tzAwareDurationMinutes` DATE_ONLY return-null path (5 tests), the strict-exact airline resolver (8 tests including the defensive "Indian Airlines must NOT resolve to AI/Air India" lock-in), and the granularity-aware chronology refine (4 tests). Existing G1 hardening test message pattern generalised for the new "arrival date must not precede departure date" wording. Backend 652/652, frontend 357/357. ### Database - **One new migration** — `20260508120000_add_aerodatabox_extended_fields`: 9 additive `ALTER TABLE ADD COLUMN` statements for the AeroDataBox extended fields (Flight: `runwayDepartureTime`, `runwayArrivalTime`, `isCargo`, `aerodataboxLastUpdatedUtc`, `aerodataboxQualityTags`, `baggageBelt`, `checkInDesk`; Airport: `shortName`, `municipalityName`). Zero-downtime safe, idempotent. ### Docs - **Roadmap restructured** — v1.5 (silent field capture) and v1.6 (importers) merged into a single V1 finale; v1.7–v1.9 (social / insights / PWA) moved into V2 as v2.3–v2.5. ## [1.4.0] - 2026-05-04 ### Added - **AeroDataBox as a third flight-lookup provider** — Closes the gap that AirLabs and Aviationstack-Free leave open: historical lookup for any date in the last 365 days, plus terminal / gate / aircraft registration / Mode-S / operating-vs-marketing carrier on codeshares. Wired into the cascade as a tertiary fallback for out-of-live-window queries and historical enrichment. RapidAPI BASIC free tier (600 API-units / month). - **Bulk historical refresh of existing flights** — Settings → API Keys gains a "Refresh all flights in 365-day range" card under the AeroDataBox key. Backfills tail number, Mode-S, codeshare metadata, distance, airline IATA/ICAO on flights entered before this provider was wired up, never overwriting curated values. A confirmation modal shows how many API calls the run will cost. Disabled on the demo account so shared keys aren't drained by tutorial sessions. - **Per-provider quota indicators with capability badges** — Each provider's quota is surfaced honestly on the API Keys page: AeroDataBox shows the live `X/600 API units` (primary) plus the secondary HTTP-request counter; AirLabs / Aviationstack-Free are labelled "provider doesn't expose quota — free tier ~1000/month"; OpenSky says "no monthly limit — IP-based rate-limit only". A "365-day lookup" capability badge marks AeroDataBox as the historical-fallback workhorse so it's clear at a glance which provider does what. - **Persisted tail number, Mode-S code, distance and codeshare metadata** — Flights enriched via lookup now carry the operating registration, the 24-bit Mode-S hex identifier, the operating airline when it differs from the marketing carrier, the great-circle distance in km, and airline IATA/ICAO. Feeds the new aircraft-hulls ranking and per-tail profile pages. - **Aircraft hulls ranking + per-tail profile pages** — Stats → Advanced gains a leaderboard of the registrations you've flown most often. Each tail number opens its own page (`/aircraft/:reg`) showing type, flights flown, total distance, and the timeline of every flight on that hull. Works against any flight with an `aircraftRegistration` (provider-sourced or manually entered). - **Currency input accepts any ISO 4217 code** — The currency picker on flight and cost forms is now a styled combobox that takes any three-letter ISO 4217 code, not just the hardcoded shortlist. Filters as you type, falls back to the raw code for exotic currencies, surfaces the symbol when known. Closes #100. - **Airline logos in flight cards and trip sidebar** — Each carrier now renders its logo next to the airline name, served from the Daisycon free aviation-logo CDN. Falls back to a letter-box monogram (LH, BA, …) for carriers not in the CDN — no broken image icons. - **Bulk export with `GET /flights?all=true`** — Skips the default 500-row cap so a script or AI agent can pull a full account snapshot in one call. Bounded only by the per-PAT rate-limit bucket; cookie-session callers still default to 100. - **Round-trip-fair Excel export** — `.xlsx` export now carries every editable field (43 columns), real numeric and date cells (instead of strings), an autoFilter on the header row, and a frozen header. Re-importing the same file is loss-free across all boarding-pass and email-import metadata that the import side already accepts. ### Fixed - **AeroDataBox quota indicator showed 2400 instead of the actual 600** — RapidAPI sends two independent rate-limit counters: `X-RateLimit-API-Units-*` (the real BASIC tier quota = 600/month) and `X-RateLimit-Requests-*` (a separate HTTP-request counter ~4× larger and irrelevant for plan budgeting). The indicator was reading the latter, so BASIC-tier users saw a misleading "2400 left" headroom. Now reads API-Units as primary and surfaces the HTTP-request count as a secondary detail only. - **Closed and decommissioned airports missing from the seed** — The seeder filtered out OurAirports rows where `type='closed'`, excluding every decommissioned IATA code (BER's predecessor SXF, TXL, etc.). Older flights re-imported via Excel or boarding-pass scan failed to enrich because the lookup couldn't find the code. Closed airports are now seeded on first install, and a one-shot boot-time backfill (`backfillClosedAirports.ts`) adds them to existing installs without re-running the full seed. - **API-key test endpoints rejected masked-input retests** — Saving a key, navigating away, returning and clicking "Test" sent the masked display value (`••••abc1`) to the test endpoint instead of the persisted key, so the test always failed with 401 even though the saved key was valid. Test endpoints now fall back to the persisted key when the input is empty or contains the mask placeholder. - **Past + scheduled flights on the same airport pair drew two overlapping arcs** — Routes carrying both flown and scheduled legs rendered two separately-tinted arcs at the same `arcHeight`, producing a visible Z-fight (Madagascar regression). The route layer now collapses both onto a single canonical arc carrying both flight IDs; the "scheduled" signal lives in a fragment-shader-driven gradient on that same arc rather than a separate midpoint marker. - **`PUT /flights/:id` silently dropped boarding-pass and email-import fields** — Field-level updates for `seatNumber`, `boardingGroup`, `gate`, `terminal`, `bookingReference`, `ticketNumber`, `baggageAllowance`, `frequentFlyerNumber`, `bookingClassLetter`, `coPassengers`, and `dataSource` were accepted by the Zod schema but discarded by the route handler. Editing any of these in the UI or via API now persists. Clients can also explicitly override `depTimeSemantics` / `arrTimeSemantics` so a `UTC` semantics flag beats the implicit default during boarding-pass re-imports. - **Health check missing under `/api/v1/health`** — Reverse proxies that pin the `/api/v1/*` prefix could not reach the unprefixed `/health` route. Both paths now respond with the same payload. - **Display version drifted across env, image, and runtime** — `APP_VERSION` was duplicated across the compose env, the build-arg, and the runtime export and could silently desync — last seen on rc.1 where System-Info showed `1.3.0` on a host whose compose was not bumped. The runtime now reads `backend/VERSION` from disk on boot as the single source of truth, strips RC suffixes (`-rc.N`, `-security-rc.N`, `-beta`, `-alpha`) from the user-facing display version, and exposes both raw `buildVersion` and tidy `appVersion` via `/api/v1/version`. The compose `APP_VERSION` env var is removed. - **Empty UI after every container restart on Unraid + similar setups (rc.5 boot-race fix)** — supervisord launched nginx and the Express backend in parallel. nginx accepted `/api/*` immediately and proxied to a port where Express wasn't listening yet, so every browser open during the 30-90 s migration + airport-backfill window cached `502 / no live upstreams` and never refetched until the user hard-reloaded. Two-layer fix: (1) supervisord now starts nginx via `scripts/wait-then-nginx.sh`, which polls backend `/health` and only `exec`s nginx when ready (5-minute fallthrough so the container never hangs forever); (2) the frontend Axios clients gained a gateway-retry interceptor that retries idempotent verbs on 502/503/504 / `ERR_NETWORK` with exponential backoff (3 attempts, 0.5/1/2 s) so any future short backend outage heals on its own. Docker `HEALTHCHECK --start-period` bumped 40 s → 180 s to absorb the longest observed first-boot path (closed-airport backfill). Verified end-to-end on a fresh local stack: 0 `no live upstreams` events in `nginx error.log`, port 80 cleanly held closed for the entire boot window, then opened with HTTP 200. ### Changed - **Routes-mode "scheduled" indicator now lives inside the arc, not as a separate dot** — Earlier 1.4.0 RCs marked routes carrying an upcoming flight with a small cyan-bordered scatter dot at the arc midpoint. The dot read as a foreign visual element on a heatmap-dense map. Replaced by an in-arc symmetric gradient: the same single arc fades from sky-blue at both ends through a heatmap-coloured core. Implemented via an `UpcomingArcLayer` that subclasses `ArcLayer` and injects ~10 lines of fragment-shader code at `fs:DECKGL_FILTER_COLOR`, using the existing `geometry.uv.x` segment ratio. Single line per route, blue-leaning baseline so an upcoming-route arc reads as primarily scheduled with a heatmap-tinted core. - **Currency picker is a styled combobox** — Replaces the native `` with a custom dropdown matching the rest of the form styling. Same accept-any-ISO-4217 behaviour, just visually consistent across browsers (native `` rendered very differently in Firefox / Chrome / Safari). - **Email-import regex warning copy is calmer** — The "automatic import is still limited" warning under email import now leads with a clearer title, separates the explanation from the recommendation with a paragraph break, and points at the Ollama setup more directly. ### Security - **Per-minute burst limiter stacked on `/airports/search`** — The autocomplete endpoint is unauthenticated by design (signup-flow needs it before credentials exist) and was already capped at 100 / 15 min. A scraper could still fire ~100 requests in seconds before the sustained ceiling tripped; a 30 / min burst limiter now layers on top so abusive enumeration is bounded both per-second and per-window. PAT-authenticated callers retain the 10× multiplier on both buckets. - **Closed 14 Dependabot alerts on the frontend lockfile** — axios bumped from `1.15.0` to `1.16.0` (declared `^1.15.2`), sweeping up 13 alerts (3 high, 9 medium, 1 low) covering prototype-pollution gadgets, CRLF injection, NO_PROXY bypass, and header injection. Backend was already on the safe range; this fix syncs the frontend. `uuid` is forced to `^14` via an override (transitive via `exceljs@4.4.0` was on `8.3.2`) — our usage only hits `uuid.v4()` so the buffer-bounds-check vuln in `v3/v5/v6` was not actually reachable, but the override is preferable to dismissing. ### Docs - **OpenAPI spec scope clarified** — The published spec is the curated stable public surface (flights, trips, airports, stats, parsers, batch import, tokens), not 1:1 with the 162-route internal API. The spec description now states this explicitly and invites users to open an issue if they need a route promoted. ### Database - **Three new migrations** — `20260503080738_add_aerodatabox_api_key` (admin- and user-level `aerodatabox_api_key` columns), `20260503154328_add_aircraft_provenance` (per-flight `aircraft_registration`, `aircraft_mode_s`, `airline_iata`, `airline_icao`, `distance_km`, `is_codeshare`, `operating_airline`, `callsign`, `status_override`), and `20260504180000_user_is_demo` (`users.is_demo` flag). All additive, idempotent (`IF NOT EXISTS`), zero-downtime safe. ## [1.3.0] - 2026-05-01 ### Removed - **Light mode and the in-app theme switcher** — TravStats now boots dark-only across the App, the marketing site, and the wiki. The Settings → Display section no longer offers a theme toggle, and `themeStore.isDarkMode` / `toggleDarkMode` / `setDarkMode` were dropped (only `mapTheme` is persisted now). Rationale: every map, globe, heatmap, and chart was tuned exclusively against the dark token set, so the light variant was always a second-class surface. Collapsing to one mode tightens contrast, removes the toggle CLS, and kills the dark/light branches in 16 components. ### Changed - **Brand amber rebalanced and per-domain colour tokens introduced** — The primary accent moved from `#e8a045` to `#f0a947` for stronger dark-surface contrast (8.40 : 1 against `#0d1117`). A new `--domain-flight … --domain-ferry` token set covers the nine planned travel domains (flight, cruise, hotel, POI, train, hike, bike, road, ferry) with WCAG-AA contrast on dark, each with `-soft` and `-locked` variants for upcoming-feature gating. App and TravStatsWeb share the same token system; the source of truth lives in `TravStatsWeb/brand/BRAND.md`. ### Added - **Public REST API with Personal Access Tokens** — External tools (AI agents, scripts, integrations) can now reach every flight, trip, airport and stat the web UI shows. Mint a PAT under _Settings → API Tokens_, then send `Authorization: Bearer ts_pat_…` on any authenticated endpoint. Scopes (`read` / `write` / `admin`) are enforced at middleware level; the plaintext token is shown exactly once on creation and stored only as a bcrypt hash. Per-token rate-limit buckets (`pat:`) keep an aggressive automation script from locking the owning user out of the web UI. - **OpenAPI 3.0 spec + Swagger UI** — Auto-generated from the existing Zod validation schemas via `zod-to-openapi`. Browse it at `/api/v1/docs` (Swagger UI) or fetch the raw JSON at `/api/v1/openapi.json`. Covers flights CRUD, trips, airports, stats, parsers (email + boarding pass), batch import, and token management. - **Boarding-pass-aware flight merge** — `POST /flights?merge=true` now fills missing fields on an existing same-day flight from incoming data without ever overwriting curated values. Boarding-pass scans, email-confirmation imports, and re-imports of partially-entered flights enrich the existing row in place instead of producing a duplicate. The dashboard surfaces a _flightMerged_ toast that reports how many fields were filled. - **Excel/CSV round-trip import + export** — Export flights as `.xlsx` with a stable column layout, edit them in Excel, re-import the same file: rows with an `id` column update existing flights, rows without create new ones. The shared CSV parser (`parseImportFile`) now handles JSON, CSV, and XLSX through the same pipeline. - **DATE_ONLY and UNKNOWN time semantics for historical flights** — Bulk imports of decades-old flights frequently know only the date, not the wall-clock time. The `flights.dep_time_semantics` / `arr_time_semantics` columns introduced in 1.2.0 now accept `'DATE_ONLY'` and `'UNKNOWN'` as first-class values on every write path (`POST /flights`, `POST /flights/batch`, `PUT /flights/:id`). The frontend hides the meaningless `00:00` time on the calendar, in the trip-details sidebar and in CSV exports for `DATE_ONLY` rows; durations fall back to a great-circle estimate (`distance / 800 km/h + 15 min overhead`, formatted as `~3h 20min`) so date-only flights still contribute realistic numbers to the Advanced-Stats aggregates instead of zero. Flight-time UI distinguishes estimated (`~`) vs measured durations. - **Agent-driven bulk-import API surface** — Three pieces lift the ceiling for AI-agent automations that import on a user's behalf. (1) Per-PAT rate-limit quotas are now 10× the cookie-session quota for airport search, single-flight create, and batch create — a sustained 152-flight import no longer trips the per-user-bucket on the second iteration. (2) `POST /admin/users` lets an admin-scoped PAT provision new accounts programmatically (Zod-validated, bcrypt-hashed password, `MAX_USERS` enforced). (3) `POST /admin/airports/reseed` re-runs the OurAirports seeder against an existing instance (`force: true` bypasses the "already populated" gate) so an agent can fill in closed/historical airports that an older OpenFlights seed missed; `GET /admin/airports/seeding-status` reports progress. - **Trip auto-detection (`POST /trips/detect`)** — A 3-stage heuristic groups flights into trips without ever overwriting curated data. (1) PNR-cluster auto-link picks up shared booking references, capped at a 30-day span so frequent-flyer IDs like a surname don't merge a year of unrelated travel into one mega-trip. (2) Home-loop proposes trips that depart from and return to the user's home airport (uses `users.home_iata` history so prior home-airport moves are respected), grouped via the multi-PNR pattern that emerged in real test data (e.g. one Hawaii trip whose outbound and inbound legs were booked separately). (3) Continuity sliding-window proposes trips for unanchored flight chains where consecutive legs match IATA hops within 7 days, with a 200 km open-jaw tolerance for "fly into Berlin SXF, take the train, fly out from BER". `dryRun: true` returns proposals without writing, `dryRun: false` commits the auto-link round and removes orphan trips atomically. The `` on the Trips tab calls the dry-run on mount and shows a single _Link All_ CTA when something is found. - **Per-leg edit, duplicate, and unlink actions in the trip-details sidebar** — Each leg row now reveals icon actions on hover: edit (✏️), duplicate, and remove-from-trip (minus-circle). The trip header itself gained edit / delete actions next to the title. DE and EN strings added (`trips.removeFromTrip`, `toasts.flightRemoved`, `toasts.flightRemoveError`, `common:buttons.duplicate`). ### Fixed - **Re-importing a boarding pass for an already-entered flight created a duplicate (issue #84)** — Different scan paths emit the same flight as `LH 123`, `LH123`, `lh123` etc. The duplicate-check on `POST /flights` compared raw strings, so re-importing slipped past as a "different" flight. Flight numbers are now normalised at the schema boundary (uppercase, no whitespace) and the duplicate-check fetches all of the day's candidates and compares normalised in JS, so legacy non-canonical rows in the DB still match. - **OpenAPI spec drifted from the actual route surface** — Corrected `/airports` → `/airports/search`, `/stats` → `/stats/summary`, swapped `PATCH /flights/{id}` for the actual `PUT`, and added the missing `GET /flights/{id}` endpoint. The Swagger UI now matches what the server serves. - **API Tokens settings page flickered + threw an i18n error** — `useCallback`'s dependency array contained the i18n `t` function, whose identity is not stable across renders, so the reload effect re-fired every render. Empty-deps + an `eslint-disable` comment that explains why. Also corrected `t("common:loading")` (which returns an object, not a string) to `t("common:loading.title")`. - **Quick-start `docker compose up` restart-looped on a clean Linux host (issue #85)** — Both `db` and `app` services dropped `cap_drop: [ALL]` without re-adding the capabilities the upstream Postgres / Node entrypoints actually need. `db` now also adds `DAC_OVERRIDE` so the post-chown `find` in the Postgres entrypoint can traverse `$PGDATA`; `app` adds `DAC_OVERRIDE`, `FOWNER`, `SETUID`, `SETGID` so the entrypoint can set up `/app/data/secrets` and drop privileges to non-root before starting Node. Verified with the verbatim README quick-start on a freshly created named volume. - **File logging silently degraded to console-only on first boot** — Boot-time scripts (airport seed, achievement registration) run with the entrypoint's UID and create `/app/data/logs/app.log` owned `root:root`. The supervised backend then runs as `node` (uid 1000) and could not append to those existing files (`EACCES`), so the Pino file transport disabled itself and only `docker logs` had any record of runtime activity. The entrypoint now does a final `chown -R node:node /app/data` after all root-phase scripts finish, so the long-running backend can persist `app.log` / `error.log` / `http.log` from the very first boot. - **README quick-start `curl` URL 404'd (issue #85 side note)** — `raw.githubusercontent.com` is case-sensitive and the default branch is `Main`, not `main`. Corrected the example URL. - **CI lint failed with "rule react-hooks/exhaustive-deps not found"** — Two competing flat-config files (`eslint.config.js` + `eslint.config.mjs`) lived side by side; the `.js` variant takes precedence in ESLint 9 flat-config resolution but did not register `eslint-plugin-react-hooks`, while the `.mjs` (which did) was silently shadowed. Consolidated into a single `eslint.config.js` with the plugin wired and `rules-of-hooks` enforced; removed the dead `.mjs`. - **Stale "Modus" reference in Settings → Display** — The section description still mentioned "Modus" / "mode" after the dark/light toggle was removed, and the orphan `settings.display.theme.*` i18n sub-tree (Light Mode, Dark Mode, lightDescription, darkDescription, autoSave, active) lingered in both DE and EN translation files. Description tightened to "Sprache, Zeitzone sowie Datums- und Zeitformat zentral steuern" / "Control language, timezone, and date/time formats centrally" and the dead keys removed. - **Scheduled flights drew the past route's heatmap colour when an airport pair had also been flown before** — `routesLayer` keys routes bidirectionally (`ADD-TNR === TNR-ADD`), so a 2026 trip to Madagascar whose airport pair already carried 2024 flown legs collapsed all four flights into one mixed route, fell through to `getHeatmapColor`, and rendered red instead of cyan. The layer now splits scheduled and past flights into separate aggregations and emits two distinct arcs on the same airport pair when both exist — past flights keep the heatmap colour (so frequent routes still pop), and the scheduled arc stays cyan so the "this is upcoming" signal survives the mix. Pure-scheduled and pure-historical routes are unchanged. - **Ollama text parser silently invented years for booking PDFs that omitted them** — Some travel agencies (e.g. Berge & Meer) print only "Mo 18 Mai 22:05" with no year. Two consecutive imports of the same PDF produced different years (April → correct 2026, May re-import → ghost 2024 flight) because the system prompt had no current-date anchor and its example date hardcoded `2024-06-10`, priming the model toward the past. The prompt now injects today's date as a year-resolution reference, instructs the model to prefer the next future occurrence when the source omits a year, and uses a current-year placeholder in the schema example. The model also returns an explicit `inferredFields` array per leg listing every field it had to guess; the import-review modal renders a yellow `!` badge next to those fields with a "please verify" tooltip so silent year hallucinations stop landing in the DB unnoticed. - **Negative and zero durations on historical bulk-imported flights** — Date-only rows previously stored `12:00 local` as a placeholder for both departure and arrival, which converted to different UTC instants for east-bound legs (`MUC → HNL` etc.) and produced negative wall-clock durations of −3 h to −9 h. Importers now write `arrUtc = depUtc` for `DATE_ONLY` rows so `arrival − departure = 0`, and the duration helper (`getFlightDuration`) prefers a great-circle estimate over a zero/negative measured delta. Existing rows with semantics `'DATE_ONLY'` were retroactively normalised by the boot-time backfill. - **Same-day flight legs sorted out of order on Hawaii-style hops** — Trip-details sidebar showed `HNL → SFO` before `OGG → HNL` on the same calendar day because `HNL → SFO` was tagged `DATE_ONLY` (12:00 UTC placeholder) while `OGG → HNL` had a real 20:58 UTC departure, and pure timestamp sort got it backwards. Replaced the fragile pairwise swap with a topological chain builder over each contiguous same-day window: pick the head leg whose `depIata` is not used as anyone else's `arrIata`, then extend by `prev.arrIata === next.depIata`. Falls back to timestamp order on disconnected windows so disjoint same-day chains stay stable. - **Round-trip names picked an arbitrary mid-trip airport** — `MUC – OGG · Dec 2025` for a Hawaii return-trip looked nonsensical because the title used the last-arrival IATA. Trip naming now detects round trips (`origin === lastArrival` or `home_loop` source) and uses the great-circle furthest waypoint with a `↺` separator (`MUC ↺ HNL · Dec 2025`); one-ways keep the `MUC – HNL` en-dash form. - **Trash icon on a trip leg deleted the flight forever** — The icon implied "remove from trip" but called `flightsApi.delete` and wiped the row from the database. Now uses `tripsApi.assignFlights({ action: "remove" })` which only clears the `tripId` association, plus a minus-circle icon to reinforce unlink semantics. The flight stays — it just goes back to the unassigned bucket where the next detection run can re-pick it up. - **Sidebar didn't refresh after unlinking a leg** — The unlink succeeded on the backend but the sidebar kept showing the removed leg until the user navigated away and back. Selection update now uses `showDetails(remaining, "trip-details")` instead of `setSelection`, which preserves `detailMode` instead of dismissing the sidebar. - **Trip auto-commit could leak orphan trips on concurrent enrichment** — If a parallel enrichment job moved a flight into a different trip between proposal generation and commit, `commitProposals` could write a trip with one or zero remaining flights. Added a TOCTOU guard: `updateMany` is filtered on `tripId: null` and the result count re-checked; if fewer than 2 flights are still unassigned, the trip + booking inserts are rolled back via `tx.delete`. Also set explicit `{ maxWait: 5_000, timeout: 30_000 }` on the `$transaction` so 50+ trip commits don't trip the default 5-second ceiling. - **Backend default `display` settings overrode browser locale detection on first login (issue #87)** — Fresh accounts on an English browser were flipped to German UI the moment `loadRemoteSettings()` fired, because the shared `defaultSettings` served when no `UserSettings` row existed yet hardcoded `language: 'de'` / `timezone: 'Europe/Berlin'` / `dateFormat: 'DD.MM.YYYY'`. The frontend's top-level spread merge then replaced the entire `display` object, wiping the locally-detected `en`. Two-part fix: (1) backend seed defaults no longer carry `language`, `timezone`, or `dateFormat` — only `theme` and `timeFormat` (the client knows the user's browser locale, the server doesn't). (2) `loadRemoteSettings()` now does a per-group shallow merge so a remote payload with missing keys preserves the locally-detected values. As a follow-up the frontend Zustand store also derives its initial defaults from `navigator.language` / `Intl.DateTimeFormat().resolvedOptions().timeZone` (en-US → `MM/DD/YYYY`, en-* → `YYYY-MM-DD`, else `DD.MM.YYYY`) instead of hardcoding German. - **Frontend formatting drift broke CI** — 16 files had Prettier formatting that diverged from `frontend/.prettierrc`. CI's `format:check` step caught it but local edits had bypassed the post-tool-use Prettier hook for those files. Ran `prettier --write` across `src/**/*.{ts,tsx,css}` to converge. ### Security - **Token mutations are explicitly cookie-only** — `POST/DELETE /settings/tokens` reject Bearer-token requests; a leaked PAT cannot self-renew or mint admin tokens. - **Admin endpoints require an `admin`-scoped PAT** — Even when the owning user has `isAdmin=true`, an automation script can only reach `/admin` when its token was minted with the `admin` scope. Account-level admin no longer implicitly elevates token scope. - **PAT write-scope enforcement on every mutation route** — `requireWriteScope` was defined in the auth middleware but never mounted on any router, so a `read`-scoped PAT could create / update / delete flights, trips, settings, parser templates, achievements, analytics events, and uploads. The guard is now method-aware (`GET/HEAD/OPTIONS` pass through unconditionally) and wired into all mutation routers — read tokens are now genuinely read-only. Cookie sessions are unaffected. - **DoS vector on `POST /admin/airports/reseed` closed** — A single admin-PAT could fire unlimited reseed requests, each kicking off a multi-second IO-bound seed against OurAirports. New `adminReseedLimiter` caps it at 3 reseeds per hour per user/IP (`ADMIN_RESEED_*` constants in `RATE_LIMITS`). - **Stale `seedingInProgress` flag could permanently lock out reseeding** — The in-memory boolean was set true on start and cleared on completion, but a process crash mid-seed left it stuck and every subsequent reseed call short-circuited with "already running". The flag is now reconciled against `airportSeedingStatus` on each call: if no `running` row exists, the in-memory flag is cleared. - **Defense-in-depth `requireWriteScope` on the admin router** — Even though `requireAdmin` already blocks any non-admin-scoped PAT, the admin router now also runs `requireWriteScope` at the router level. Future-proofs against an intermediate scope (e.g. `admin-read`) being introduced — admin GETs still pass for read-style tokens, mutations stay gated. ### Tests - **+38 tests** — 6 for the API-token utility (lookup hash collision-resistance, bcrypt verify, prefix regex, plaintext format), 6 for the OpenAPI spec build (paths, security scheme, BearerAuth, methods on `/flights/{id}`), 2 for flight-number normalisation, 3 for the boarding-pass merge utility, plus 21 for the `requireWriteScope` middleware (cookie passthrough, read-token GET/HEAD/OPTIONS pass, read-token POST/PUT/PATCH/DELETE 403, write/admin-token full passthrough on every method). - **+14 tests for trip detection and DATE_ONLY duration** — 10 unit tests covering `tripDetectionService` internals (PNR clustering, 30-day span cap, cancelled-duplicate suppression, home-loop with/without history, continuity 7-day window, gap-break, 200 km open-jaw); 4 unit tests covering `getFlightDuration` (real `UTC` time, `DATE_ONLY` estimate, missing time falls back to estimate, negative-delta guard). - **+5 tests for `sortFlightsByLegOrder`** — chronological pass-through, 2-leg DATE_ONLY/timed reversed pair, 3-leg same-day chain repair, no-swap when current order already chains, and disjoint same-day chains keeping timestamp order. ## [1.2.1] - 2026-04-28 ### Fixed - **Flight number lookup returned today's schedule for past or future dates (issue #82)** — `/api/v1/flight-lookup/:flightNumber` previously went straight to AirLabs `/schedules`, whose free tier silently ignores `dep_date` and returned today's data for any requested date. The route now consumes the full provider cascade and reports three distinct unavailable reasons: `no_provider` (free tier can't serve non-today and no Aviationstack key is configured), `no_match` (live lookup empty — likely a typo), and `no_match_api_gap` (provider returned today's schedule for a non-today request — the issue-#82 symptom). The frontend keeps the user on the input step with a reason-specific error message instead of jumping silently into manual entry. The OpenSky-30-day-historical claim was dropped from the user message and from the in-form tooltip — the underlying `/api/flights/callsign` endpoint does not exist (404), so OpenSky now stays in the live cascade only. ### Tests - **+9 regression tests for `lookupFlightWithHistorical`** — Cover the three unavailable reasons, the smoking-gun heuristic for "today's schedule for non-today" responses (yesterday / tomorrow / far past), and the live-window happy path. ## [1.2.0] - 2026-04-27 ### Fixed - **Reminder emails for manually-entered flights fired 1–2 h off** — Manual entries, parser results, and CSV imports stored departure/arrival times as wall-clock-as-fake-UTC, while API-imported flights stored real UTC. The reminder scheduler compared `now` (real UTC) to a mixed-convention `departureTime` column, so the 24-hour-before email triggered an hour early in CET, two hours early in CEST, etc. All flight create/update paths now write canonical real UTC built from a `(local datetime, IANA timezone)` pair, and the scheduler normalises legacy rows on the fly via the airport timezone before the ±15-minute window check. - **Schema drift between `prisma/schema.prisma` and prod migrations reconciled** — The drift file now uses idempotent `IF EXISTS` drops, backfills NULLs before flipping NOT NULL, and guards every foreign-key add. Previously a fresh deploy could fail mid-migration on rows with NULL `has_live_tracking` or `historical_enrichment_*` flags. ### Changed - **Flight write contract is now `(local, timezone)` pairs** — `POST /flights`, `PUT /flights/:id`, and `POST /flights/batch` no longer accept ISO datetime strings. Clients send `departureLocal` (`YYYY-MM-DDTHH:mm`) plus `depTimezone` (IANA, e.g. `Europe/Berlin`); the server runs `fromZonedTime` to derive the canonical real-UTC `Date` it stores. The update endpoint also rejects raw `departureTime` / `arrivalTime` fields. Frontend submit, edit, review, duplicate, and CSV import flows were updated end-to-end. - **Time-semantics tagging during the cutover** — Every new write is tagged `dep_time_semantics = 'UTC'`. Existing rows default to `'UNKNOWN'` and are left unchanged by the normaliser until the backfill script reclassifies them as `'UTC'` (API-sourced) or `'LEGACY_FAKE_UTC'` (manual/parser-sourced, converted in place). `tzAwareDurationMinutes` short-circuits when both sides are UTC and only re-interprets when both are explicitly LEGACY. ### Added - **In-app update banner** — A pulsing yellow `Update` badge appears next to the wordmark in the header whenever a newer stable GitHub release exists. Click it to open a popover with the version number, release date, a 600-character preview of the release notes, a link to the full notes on GitHub, and an `Ignore this version` action that hides the badge until an even newer release ships. Backend caches the GitHub query in process memory for 6 hours, filters out RC and pre-releases, and degrades to "no banner" when the GitHub call fails — so air-gapped or firewalled instances stay quiet rather than throwing errors. ### Removed - **Floating `?` help icon on the dashboard** — Removed the corner help button and its three orphaned i18n keys (`dashboard:map.help2d/help3d/helpExpanded`). It only said "Switch between 2D and 3D" — information that's already visible from the mode selector. ### Database - **`flights.dep_time_semantics` / `arr_time_semantics`** — Two new `TEXT NOT NULL DEFAULT 'UNKNOWN'` columns tag the storage convention of each row during the cutover. Migration is additive and idempotent (`IF NOT EXISTS`); zero-downtime safe. ### Migration - **Backfill runs automatically at container boot** — `docker-entrypoint.sh` invokes `dist/scripts/backfillTimeSemantics.js --apply` once migrations succeed, so existing self-hosted instances pick up the fix on the first start of `1.2.0` without any manual SSH step. The script is idempotent (`WHERE dep_time_semantics='UNKNOWN' OR arr_time_semantics='UNKNOWN'`), so subsequent boots return zero rows and finish in milliseconds. Disable with `TIMESEMANTICS_AUTO_BACKFILL=false` if you prefer to run it by hand. Manual invocation remains available: `node /app/backend/dist/scripts/backfillTimeSemantics.js [--apply]` (defaults to dry-run). On the dev DB the backfill classified 160 flights (37 UTC, 123 LEGACY converted, 0 errors); production numbers will differ per install. ### Tests - **+37 tests covering the new contract** — 11 covering `legacyFakeUtcToRealUtc`, `normalizeFlightTimeUtc`, and semantics-aware `tzAwareDurationMinutes` (CEST / CET / JST / LAX→JFK transcontinental). 9 covering the new Zod schema (paired contract, half-pair rejection, IANA validation, malformed local, legacy-field rejection, historical exemption, partial updates). 8 reminder-scheduler and flights-route integration updates. 9 covering the update-banner semver comparator (rc/beta stripping, edge cases, malformed inputs). ### Docs - **Workflow revised** — Branching strategy in `CLAUDE.md` is now scoped to change size (direct `main` / `fix/*` / `feat/*` / `dev/*`); long-running dev branches must merge `main → dev` early and often, never the reverse, and never touch `VERSION` or `CHANGELOG.md` (both owned by `/deploy` on `main`). ## [1.1.0] - 2026-04-18 ### Added - **Birthday Flight achievement** — New `BIRTHDAY_FLIGHT` achievement counts flown flights that depart on the user's birthday (month + day, year irrelevant). Profile settings now include an optional birthdate field used by the check. - **Kurios calendar easter eggs** — Six hidden achievements tied to aviation and calendar observances: ICAO Day (7 Dec), Wright Day (19 Aug), May the Fourth (4 May), Pi Day (14 Mar), Pi Precision (3 141 km ± 5 % on Pi Day) and Halloween (31 Oct). - **Branded GlobeLoader** — Monochrome spinning-globe loader replaces the generic spinner at every large-area loading surface (maps, stats, flight pages, admin). Paints in < 16 ms so users see feedback before deck.gl / three.js finish booting, holds for ≥ 2 s to avoid flashing, and keeps spinning regardless of `prefers-reduced-motion`. Reads live CSS tokens so it reacts to dark-mode toggles without remount. - **New logo system (v1.0)** — Luggage-tag mark with `TS` monogram and a cross-dot `TRAV✛STATS` wordmark ship as `LogoMark`, `LogoMarkFilled`, `LogoWordmark` and `LogoLockup` components. Visible in the nav header (desktop + mobile), all four auth screens and the favicon (SVG with dark background + PNG fallback). The README and Unraid forum / release docs point at `docs/images/logo.svg`. - **App version vs build version split** — Runtime now exposes the deployed app version (from `backend/VERSION`) and the Docker image build version as distinct fields, so About can show `1.1.0 (built from 1.1.0-rc.3)` during the RC cycle. ### Changed - **Achievements modules split** — `backend/src/utils/achievements.ts` (1 073 lines) reorganised into three files (orchestrator + stats + checks); `backend/src/data/achievements.ts` (1 306 lines) split into two seed parts plus a thin composition layer. Public API unchanged. Every file now respects the 800-line cap. - **Achievement stats are immutable** — `checkAndUpdateAchievements` builds a fresh `augmentedStats` via spread rather than mutating the object returned from `calculateUserStats`; `Set`s are cloned, never mutated in place. ### Fixed - **GlobeLoader no longer freezes for reduced-motion users** — The loader is feedback, not decoration; it now always animates. - **Favicon visible on light browser chrome** — The new SVG favicon shipped transparent, which made the amber strokes disappear on Safari / iOS tab bars. A dark background is now baked into the SVG. - **`LogoMarkFilled` survives theme switches** — Inverse colour used to be hardcoded `#0b0d10`; now reads `var(--bg-base)` so the mark stays legible in light mode. - **Screen-reader announcements for the header logo and mobile drawer** — Home link now announces as "TravStats — Home" once, not the visible text three times. Mobile drawer Donate / Star links carry the `aria-label` attributes their desktop twins already had. - **`GlobeLoader` canvas no longer churns on parent rerenders** — Derived `buffer` dimension is memoised so the effect dep array is stable. ### Database - **`users.birthdate`** — New nullable `TIMESTAMP` column backing the Birthday Flight achievement. Additive only; no data transform; safe to deploy with zero downtime. ### Tests - **Brand/Logo component suite** — 9 new Vitest tests cover the four logo components (a11y attributes, theme-token defaults, layout modes). Total frontend coverage is now 257 tests across 54 files. ## [1.0.1] - 2026-04-17 ### Security - **Base images refreshed and patched** — Builders moved from `node:20-alpine` to `node:22-alpine`; production stage moved from `node:20-slim` to `node:22-bookworm-slim`. The production image now also runs `apt-get upgrade` at build time so every rebuild pulls the latest Debian security feed on top of the base layer. Closes the Critical CVE-2026-6100 and eleven High-severity npm CVEs (tar, minimatch, cross-spawn) that Docker Scout reported against `v1.0.0`. Drop-in replacement — no schema, config, or runtime behaviour changes. - **CI aligned with runtime** — GitHub Actions now runs backend and frontend checks on Node 22 to match the container image. ## [1.0.0] - 2026-04-16 First stable release. TravStats exits its public beta period after roughly six months of daily use and a 22-finding black-box pentest (all findings mitigated). The surface below is what ships out of the box. ### Core tracking - Manual flight entry with categories, tags, up to 50 travel companions, costs and currency - Five flight states — `flown`, `scheduled`, `cancelled`, `historical`, `duplicated` — each with dedicated forms and validation - Timezone-aware durations via `date-fns-tz` - Duplicate detection (same `flightNumber` × day) with a confirmed "save anyway" escape hatch and a dedicated `duplicated` status that skips time validation ### Boarding-pass scanner - QR code, PDF417 barcode and OCR fallback (Tesseract) with automatic airport/airline resolution - Camera capture and image upload, desktop + mobile ### Email and PDF import - Template-based parsing with annotation-driven template builder for new airlines - Optional local LLM parsing via Ollama (default model `gemma3:12b`, benchmarked 100% accuracy on the TravStats test corpus) - Support for plain text, HTML email, Outlook `.msg` and `.eml` - Parser rate metrics per template and per LLM model in the admin UI ### Automatic flight-data lookup - AirLabs as primary source, OpenSky OAuth and Aviationstack as fallbacks - Live gate, terminal, actual-departure, actual-arrival tracking while a flight is in progress - Pending-update inbox with per-flight statistics-impact preview; approve, edit, reject - Historical enrichment scheduler backfills gate/terminal/aircraft data for past flights - Configurable per-user: approval gate on/off, check interval, only-during-flight window ### Maps and visualisation - Six visualisation modes: Routes (ArcLayer), Heatmap, Hexagon (3D), 3D Columns, animated Trips, 3D Globe - Deck.gl 9.x + MapLibre GL 5.x using `MapboxOverlay` for a shared WebGL context - Two-stage route/trip popup → sidebar detail view - Scheduled flights rendered as cyan arcs and excluded from stats until flown ### Statistics - Year-over-year comparison, year-filter and all-time totals clearly separated - Seat distribution (window/middle/aisle/zone/class), distance, flight time, costs, top routes, top airlines - PDF year report with year auto-selected to the most recent flown year - Downloadable vintage-passport PNG certificate: hero-km number, equivalences ("N × around the Earth"), deterministic serial, layered radial gradients compatible with `html2canvas` ### Gamification - 58 Battlefield-style achievements across 5 categories (Sammler, Distanz, Elite, Entdecker, Survivor) + Planner and Special tiers - Automatic unlock on milestone reach, readable locked cards on the achievement page ### Admin and operations - Invite-only user management with per-invitation tokens (can't be reused) - Admin-triggered user deletion with cascade (self-delete and last-admin delete blocked) - SMTP configuration with test-connection - Encrypted API-key storage (AirLabs, Aviationstack, OpenSky, SMTP password) - Automated database backups with retention (`daily`/`weekly`/`monthly`) + optional WebDAV sync (Nextcloud, HiDrive, …) - Backup restore via `pg_dump` with argument-array `spawn()` (no shell injection) - Parse-log statistics dashboard (template hit rate, LLM hit rate per model) ### Bug reporting - "Bug" button in the top navigation generates an anonymised diagnostic JSON bundle in-browser - Bundle v2 ships: time-windowed log tails (24 h app / 7 d error) from live and rotated `.log.gz` files, per-user flight-state aggregates (byStatus counts + pipeline counters), allow-listed user settings (no credentials) - Server-side scrubbing of IPs, emails, JWTs and UUIDs; user IDs replaced with short opaque markers - One-click "Report Bug" copies the bundle to the clipboard and opens a pre-filled GitHub Issue Form ### Export and import - CSV, GeoJSON, KML (Google Earth) export from the admin panel - Email notifications: configurable 24 h / 2 h pre-departure reminders (SMTP) ### Internationalisation - Full German and English UI via react-i18next - German as primary locale, English as the mirror translation ### Security - JWT stored in an `HttpOnly`, `SameSite=Strict`, `Secure`-aware cookie; no Bearer fallback - 15 distinct rate limiters across auth, external-API-backed routes and admin exports - Zod validation on every input endpoint; Prisma-parameterised queries; Helmet CSP; `server_tokens off` - 22 pentest findings (2 CRITICAL, 5 HIGH, 8 MEDIUM, 7 LOW) surfaced and mitigated across the beta; see [SECURITY.md](SECURITY.md) - Patched four moderate-severity transitive CVEs via `npm audit fix` (GHSA-r4q5-vmmm-2653 `follow-redirects`, GHSA-39q2-94rc-95cp `dompurify`, GHSA-j452-xhg8-qg39 `protocol-buffers-schema`). Lockfile-only; no behaviour change. - Hardcoded LAN IP in the Ollama URL placeholder (admin UI, German + English) replaced with `http://localhost:11434`; the corresponding benchmark note in `backend/OLLAMA_OPTIMIZATION.md` and the default in `scripts/parse-samples.mjs` were scrubbed at the same time. - JWT secret and AES-GCM encryption key are auto-generated on first boot and persisted to `/app/data/secrets/` (mode 0700, key files 0600), inside the single data volume. Pre-1.0 installs on the dedicated `/app/secrets` mount are migrated once at entrypoint time. ### Zero-config install - The Docker compose file now requires **one environment variable only** — `DB_PASSWORD`. Everything else (instance name, public URL, user cap, registration mode, API keys, Ollama, backup schedule, WebDAV) is captured by the first-run setup wizard or configured later from the admin UI. - `AdminSettings` gains nine new columns (`instance_name`, `max_users`, `allow_registration`, `frontend_url`, `webdav_*`). The WebDAV password is AES-GCM-encrypted at rest via the same helper used for the other API keys. - Setup wizard extended: instance name, public URL (pre-filled from the request origin), user cap and registration toggle are captured alongside the admin username/password. - New admin section "Instance" for the instance-level fields. WebDAV sync moved into the existing "Backups" admin section with a dedicated password field that is never echoed back to the client. - Legacy `INSTANCE_NAME` / `MAX_USERS` / `ALLOW_REGISTRATION` / `FRONTEND_URL` / `WEBDAV_*` environment variables are still read as a one-time fallback for pre-1.0 deployments, then superseded as soon as an admin saves from the UI. - Secrets and data consolidated into a single `/app/data` volume. The `/app/secrets` mount is gone; JWT and encryption keys live at `/app/data/secrets/` inside the same persistent volume. Unraid, compose and Docker Desktop users now manage one mount point instead of two. Pre-1.0 installs with the old split layout are migrated automatically at boot. - Unraid Community Apps template slimmed down to two configurable knobs — `DATABASE_URL` (required) and `TZ` (advanced). `OLLAMA_URL`, `COOKIE_SECURE` and `CORS_ORIGIN` were dropped from the template: Ollama is wired up from the admin UI, cookie-secure is auto-detected from the reverse-proxy `X-Forwarded-Proto` header, and CORS defaults to same-origin behind a proxy. All three are still honoured as environment overrides for exotic setups. ### Breaking changes from 0.x beta - Docker image is published to **GHCR** (`ghcr.io/abrechen2/travstats`) as the primary build target. Starting with 1.0.0 final, `X.Y.0` releases are also mirrored bit-identically to **Docker Hub** (`docker.io/abrechen2/travstats`) for discovery. Pre-releases and patches live on GHCR only. - The default LLM model changed from `qwen2.5:7b` to `gemma3:12b`. Pull the new model with `docker exec travstats-ollama ollama pull gemma3:12b` or set `OLLAMA_MODEL` explicitly. - Parser-feedback collection and the template-correction table were removed. If you had custom code consuming `analytics_events` parser rows, it no longer populates. - `.env.prod.example` has been shrunk to five variables (one required). Existing values continue to work, but the example no longer advertises them. ## [0.29.0-beta] - 2026-04-16 ### Added - **One-click Report Bug flow** — The diagnostic-export modal's former "Issue öffnen" link is now a primary "Fehler melden" / "Report Bug" button. Clicking it copies the JSON bundle to the clipboard and opens a new tab at a GitHub Issue Form with the `version` field pre-filled and the `bug` label already applied. A toast confirms the clipboard step so the paste target is obvious. If the clipboard write is blocked (non-secure context, permissions, etc.), the issue URL still opens and the user is directed to the Download button as a fallback. - **Structured GitHub Issue templates** — New `.github/ISSUE_TEMPLATE/` entries for "Bug Report" (7 fields: summary, actual, expected, reproduction, version, diagnostic bundle, additional context) and "Feature Request" (4 fields). `blank_issues_enabled: false` steers non-maintainer contributors through one of the two templates so every incoming issue arrives pre-structured. ## [0.28.0-beta] - 2026-04-16 ### Added - **Diagnostic Export v2 — self-sufficient bug-report bundle** — The JSON bundle produced by the "Diagnose-Export" admin action now includes two new sections so a GitHub issue rarely needs follow-up questions: - `settings` — the reporter's auto-update and historical-enrichment preferences (booleans and integers only, credentials are never projected). Defends against future schema additions via a hand-written allowlist. - `flightState` — per-user aggregates (`byStatus` counts + pipeline counters including `withNextApiCheck`, `withPendingUpdates`, `withLiveTracking`, `withActualTimes`, `zombieCandidates`) so the maintainer can see at a glance what the scheduler is looking at. Log tails are now time-windowed instead of entry-capped: 24 h for `appTail`, 7 d for `errorTail`. Rotated `.log.gz` files are read transparently; a broken archive is skipped rather than breaking the whole bundle. Pino's duplicate `time`/`timestamp` pair is deduped on read, cutting bundle size by roughly 50 %. Caps (5 000 entries / ~2 MiB) prevent a flooded log from blowing up the download. Each section is collected in isolation — a DB hiccup on one section now returns `{ error: "failed to collect
" }` for that slice only, the rest of the bundle still succeeds with HTTP 200. ## [0.27.2-beta] - 2026-04-16 ### Fixed - **Historical enrichment stayed pending forever** — When users had turned off the approval gate (`autoUpdateRequireApproval=false`), newly created historical enrichments were nonetheless left as "pending" and never surfaced on the flight. The scheduler now mirrors the live auto-update path and auto-applies them. - **Zombie-flip missed flights with corrupted arrival time** — The stale-scheduled safety net only considered `arrivalTime + 6h` and was silent when a bad API lookup pushed `arrivalTime` into the future. Added a second `departureTime + 30h` trigger that flips the flight regardless of arrival-time state (30h comfortably covers ultra-long-haul like SIN→JFK). - **Gate, terminal and actual departure/arrival never persisted** — `lookupFlightDetails` silently dropped per-flight live fields (gate, terminal) by letting the static airport record shadow them, and `actualDeparture`/`actualArrival` were missing from the snapshot pipeline entirely. Both AirLabs and Aviationstack paths now forward these end-to-end, and `delayMinutes` is recomputed on apply. ## [0.27.1-beta] - 2026-04-15 ### Fixed - **Duplicate flight: airline + flight number nullToUndef** — Older imported flights have these columns NULL in the DB, but the frontend Flight type lies and types them as required strings, so they slipped past the previous nullToUndef fix. All four nullable-typed-as-required fields now go through the helper. - **Certificate: black vertical bands in exported PNG** — html2canvas does not render the SVG `feTurbulence` filter consistently and produced solid-black stripes from the noise data URI. Replaced with two layered radial gradients that give the parchment depth without the rasteriser hazard. Also removed `mixBlendMode: multiply` on the postmark for the same reason. - **Stats page: clear separation between year-filter and totals** — Both four-card rows looked identical. Added a labelled separator above each row ("── Jahr 2026 ──" / "── Gesamt (alle Jahre) ──") with a subtle bottom-border so the scope is unambiguous. - **Diagnostic export "Copy" worked on HTTPS only** — `navigator.clipboard` requires a secure context. Falls back to the legacy `execCommand('copy')` with a temporary off-screen textarea on plain HTTP. - **Bug-Report button hidden on narrower screens** — Was tucked into the `xl:` breakpoint group with Donate/Star. Lifted out so it shows on all desktop widths. - **/settings rate limiter removed** — The 1000/15min cap kept tripping during normal use. Real rate-limiting belongs on auth (brute-force), external-API-backed routes (cost) and admin exports (DB-wide reads), not on a user reading their own preferences. - **Historical-flight month "Unbekannt" snapped back to Januar** — Storing `YYYY-01-01` for both January and "unknown" meant the parser couldn't distinguish them. Now stores year-only (`YYYY`) for the unknown-month case and expands to `YYYY-01-01` only at submit. ### Changed - **Removed parser-feedback collection** — Same anti-pattern as the already-removed pattern-updater: data was collected into `analytics_events` but no longer consumed automatically. With Ollama as primary parser, the feedback was unused. Removed the `parserFeedback` service + route, the `/admin/parser-feedback/stats` and `/details` endpoints, the `/admin/parse-logs/promote` endpoint, the `FeedbackAnalytics` admin tab and the parser-correction submission from the FlightReviewModal. The `ParseTrainingLog`-based parser hit-rate dashboard (a different system) is unchanged. ## [0.27.0-beta] - 2026-04-15 ### Added - **Anonymous diagnostic export for GitHub bug reports** — A new "Bug" button in the top navigation opens a modal that generates a sanitised JSON bundle of recent app and error log entries plus system info. IP addresses, email addresses, JWT tokens and UUIDs are scrubbed server-side; user IDs are replaced with short opaque markers. Three actions: copy to clipboard, download as file, or open the GitHub issue page. - **Save + add return flight** — A second submit button on the manual flight form saves the outbound and immediately prepares the form for the return leg. Departure and arrival airports are swapped, airline / category / tags / companions carry over, and leg-specific fields (flight number, aircraft, terminal, gate, seat, times) are cleared so the user fills them in. - **Redesigned flight certificate** — The shareable PNG certificate has been completely redesigned around a vintage aviation passport aesthetic: parchment paper with grain texture, deep ink-blue typography, burnished bronze accents and a faded vermillion postmark stamp. New typography (Fraunces, Big Shoulders Display, JetBrains Mono), the total kilometres rendered as a dominant hero number, equivalence captions ("X × around the Earth at the equator"), and a deterministic 6-character serial number per certificate. - **Admin panel: delete user** — Admins can now permanently delete users from the user-management table. Cascade clears the user's flights, trips, achievements, settings and pending updates. Self-deletion and last-admin deletion are blocked. ### Fixed - **PDF year report button no longer permanently greyed out** — The advanced statistics page now auto-selects the most recent year as soon as flights have loaded, so the "PDF Jahresbericht" button is enabled out of the box instead of waiting for the user to discover the year dropdown. - **Historical flight year input accepted keystrokes** — Typing a digit into the year field for a historical flight no longer cleared itself. The form re-derived the visible year from a stored YYYY-MM-DD string with a strict 4-digit regex; relaxed to accept partial input so the field echoes each keystroke. - **Diagnostic export modal no longer trips its own rate limit** — The fetch effect re-ran on every parent re-render due to unstable `t` and `addToast` dependencies, exhausting the 10/hour rate limit within seconds. Now fires once per modal open. - **Duplicate flight succeeds again** — The "duplicate as same / return flight" action sent `null` for unset optional fields, which Prisma's optional-but-not-nullable Zod schema rejected. Nullable values are now coerced to undefined, and duplicates are created with the dedicated "duplicated" status (no time-fields requirement) so the user fills in dates in the edit modal that opens immediately. ### Changed - **Admin panel cleanup** — Removed the dead Hardware-info section, dropped the unused TemplateCorrection table, and retired the orphan pattern-updater / pattern-analyzer infrastructure that never actually applied any patterns. Tabs and i18n strings simplified accordingly. - **Certificate layout** — Footer no longer overlaps the "of note" rows; postmark stamp was repositioned tighter into the corner with a multiply blend so it reads as ink on parchment. ## [0.26.0-beta] - 2026-04-15 ### Added - **Duplicate flight action** — Each row in the flights table now has a "Duplicate" dropdown with two options: "As same flight" (copy route) or "As return flight" (swap departure/arrival). Airline, flight number, aircraft, category, tags, companions and notes carry over; time, seat, gate, terminal, booking references and pricing are cleared so the user fills in the trip-specific details. The edit modal opens automatically on the new flight. ### Fixed - **Auto-update endless loop** — When "require approval" was disabled the scheduler created a pending update on every check but never applied it, so the same changes were re-detected and re-written every 10-15 minutes. Updates with approval off are now applied immediately while the pending row is still kept for audit. - **API check cadence** — Each flight used to be polled every 15 minutes while in the air, burning 30-50+ API calls per long-haul. Scheduling now uses exactly three checkpoints: 30 minutes before departure, 60 minutes before scheduled arrival, and 30 minutes after arrival. - **Zombie-scheduled cutoff** — Flights stuck on "scheduled" past their arrival are now auto-flipped to "flown" after 6 hours (was 48h), so they stop showing up in the API-check queue. - **Scheduler log output** — The `significant_changes_found` log line now lists real field names (e.g. `gate`, `terminal`, `aircraft`) instead of array indices. ## [0.25.4-beta] - 2026-04-14 ### Fixed - **Settings page no longer trips its own rate limit** — The auto-save effect listed `t` (from useTranslation) in its dependency array, which is unstable across renders, so the debounced save was firing on every keystroke instead of only when units actually changed. Combined with new banner/home-airport fetches this hit the `/settings` rate limit (60→200→1000) within a couple minutes of normal use. Removed the unstable dep and bumped the limit to 1000/15min so background polling has plenty of headroom while still blocking scripted abuse. ## [0.25.3-beta] - 2026-04-14 ### Fixed - **Autocomplete shows both active and closed airport for shared codes** — Searching "MUC" now returns Munich Airport (active) followed by Munich-Riem (closed) instead of only the active one. The exact-match step returns all rows with the same IATA/ICAO ordered by `isClosed ASC` so users can pick the historical predecessor for old flights. ## [0.25.2-beta] - 2026-04-14 ### Fixed - **Closed predecessor IATA assignment** — When a closed airport's keywords list a single 3-letter token, that's almost always its own historical IATA (Munich-Riem `EDDM, MUC, XMUC` → MUC). When multiple are present the first is usually the successor (Tempelhof `BER, EDDI, THF` → BER is Brandenburg, THF is the closed one), so prefer the candidate not used by an active airport. Both cases now resolve correctly. ## [0.25.1-beta] - 2026-04-14 ### Fixed - **Closed predecessors no longer overwrite active airports** — Munich-Riem (closed) shares ICAO EDDM and IATA MUC with the active Munich Airport; the seeder was overwriting the active row with the closed predecessor's data, breaking searches for "MUC". The Airport schema now uses composite uniqueness on `(iata, isClosed)` and `(icao, isClosed)` so both rows can coexist, and the seeder upserts by the composite key. Closed airports are still listed alongside their successors in autocomplete with the "geschlossen" badge. - **Airport autocomplete prefers active over closed** — Search and lookup now order by `isClosed ASC` so a typed code like "MUC" returns the active airport first, with the closed predecessor below. ## [0.25.0-beta] - 2026-04-14 ### Added - **Home airport with relocation history** — Pick a home airport once during onboarding and change it later under Einstellungen › Heimatflughafen. A move never overwrites the past: every flight keeps the home airport that was active at its date, so old statistics stay truthful. - **Copy-down date and estimate arrival time buttons** — Two new icon buttons in the Add Flight and Edit Flight forms: a down-arrow that copies the departure date into the arrival date, and a calculator that estimates the arrival time from the great-circle distance using the arrival airport's local timezone. A "+1 Tag" hint appears below the arrival date for overnight flights. - **Flughäfen statistics section** — New stats card group with distinct airports, countries, continents (5/6), top 5 most-visited airports, top countries, rarest airports (visited only once), new airports this year, farthest airport from home, and a continent breakdown bar chart. Plus a new "Kürzester Layover" companion stat. - **Closed airports in autocomplete** — Permanently closed airports like Berlin Tegel (TXL) are now seeded too and remain selectable for historical flights, marked with a "geschlossen" badge. ### Fixed - **Längster Layover no longer counts living at home as a layover** — Capped at 24 hours and arrivals at the home airport active on that date are excluded. Old: "47944.6h in MUC" (5.5 years). New: realistic transit times only. - **Historical flight year input no longer locks to NaN** — Parsing the stored date with `new Date()` could produce `Invalid Date`, then `getFullYear()` wrote "NaN" back into the input and froze it. Now parsed directly from YYYY-MM-DD with regex. ### Changed - **Settings rate limit raised** — From 60 to 200 requests per 15 min. The previous limit was tripping on normal navigation now that the settings page has more sub-sections and a new home-airport banner. ## [0.24.4-beta] - 2026-04-14 ### Fixed - **Aviationstack now respects Free-tier budget** — Blocks Aviationstack calls outside the flight's live window (±3h of departure, in-flight, or 2h post-arrival) and caps them at an admin-configurable daily budget (default 3/day, enough to stay inside the Free tier's 100/month). Outside the window — the bulk of daily polls, which are just schedule lookups from T-24h to T-6h — the lookup goes straight to AirLabs, which has a much larger Free quota. An 8-flight test account now consumes about 5–8 Aviationstack calls over a 24h window instead of 240+. Skip reasons (`outside_live_window`, `daily_budget_exceeded`, `cooldown`) are logged at every lookup for debuggability. - **New admin setting:** `aviationstackDailyBudget` (default 3). Migration adds the column; set to 0 to disable Aviationstack entirely and rely solely on AirLabs + OpenSky. ## [0.24.3-beta] - 2026-04-14 ### Fixed - **Historical flights with unknown date are enrichable again** — `findEnrichmentCandidates` queried `departureTime: { gte: maxAgeDate }`, which in Prisma/Postgres also excludes `NULL`. Historical flights (the primary use case: "I flew this but don't remember when") were therefore never candidates for enrichment, even when a full reference pool existed. Now the query accepts both dated and dateless flights, and `getEnrichmentMode` returns `slim` for null so dateless flights use the conservative ICAO + terminal-only aggregation. ## [0.24.2-beta] - 2026-04-14 ### Fixed - **Enrichment bootstrap deadlock resolved** — The auto-update and historical enrichment pipelines never produced results on fresh accounts because of a chain of five interacting bugs: the significance filter dropped first-fill gate/terminal changes, `hasLiveTracking` never got seeded (gating all future enrichment), scheduled flights past their arrival time became permanent zombies, and Aviationstack times without seconds silently failed to parse. Each is now fixed — first-fill changes count as significant on their own, `hasLiveTracking` flips on any non-empty API response, a new zombie-transition job flips `status=scheduled` → `flown` after arrival + 48h (marked `lastModifiedBy='zombie_auto_flown'`), and the time parser now accepts the space-separator-without-seconds format observed from the live API. - **Aviationstack 429 retry storm prevented** — The free tier is 100 requests per month. A single 429 previously triggered aviationstack retries on every 5-minute scheduler tick. Now a 1h in-memory cooldown blocks Aviationstack after a 429, falling through to AirLabs. - **OpenSky credential validation** — If stored credentials decrypt to an unusable pair (neither full OAuth2 client credentials nor full basic auth), the resolver now returns `null` instead of an empty object so downstream logs no longer lie about configuration state. ### Changed - **Historical-enrichment threshold lowered for slim mode** — Flights aged ≥ 1 year now require only 3 reference flights instead of 5 for aggregation. Full mode (< 1 year) keeps the 5-reference minimum. This unlocks enrichment for niche routes in small deployments where 5 same-flight-number references would be unreachable. ### Docs - Added three post-V1 roadmap items surfaced by the v0.24.1 critical review: route clustering to replace per-dimension median aggregation, confidence calibration via user-feedback loop, and a cross-user consent model for future multi-tenant deployments. ## [0.24.1-beta] - 2026-04-14 ### Security - **Admin password reset via SMTP (Pentest H4)** — When the target user has a notification email on file and SMTP is enabled, the generated temporary password is now delivered via email instead of being returned in the HTTP response body. Falls back to response body for deployments without a mail server. Remaining deferred pentest items (H5 leaderboard enumeration, M7 TLS, L2/L3 SSH, L4 Dozzle) are now formally documented as accepted — by-design for the family tracker or infrastructure-level outside the repository. ### Fixed - **Map-only filter hidden on list pages** — The "min times flown" route-frequency slider only affects the deck.gl route layer. It no longer appears on the flight list page, where it was a silent no-op that misled users into thinking their list was filtered. ### Changed - **Dead canTrainLLM flag dropped** — Removed an unused permission relic of the deleted LLM training pipeline. The field was never set or read by the backend, so the frontend gate always evaluated to false. Parser access is now gated exclusively by `isAdmin`. Prisma migration drops the column; `hasTrainingAccess` renamed to `hasParserAccess`. ### Tests - **+21 new service tests** — covers email service (password reset, invitation, admin reset), reminder scheduler (dedupe, per-window error isolation, user opt-out), and cloud sync service (WebDAV upload, download, list, with all failure paths). ## [0.24.0-beta] - 2026-04-13 ### Security - **Shell injection in backup service fixed** — Replaced `execAsync` with `spawn` on Unix pg_dump paths to prevent command injection via crafted database URLs (C1). - **API credentials no longer leaked in PUT response** — Admin API key update endpoint now returns masked values instead of decrypted plaintext secrets (C2). - **Removed `targetDatabaseUrl` from restore API** — Eliminates SSRF and command injection amplification via admin-supplied database URLs (H3). - **Rate limiting added to flight lookup and enrichment endpoints** — Prevents external API quota exhaustion and database load abuse (H1, M5). - **Analytics event types whitelisted** — Only `parser_feedback` and `pattern_suggestion` are accepted, preventing pipeline poisoning (H2). - **13 additional fixes** — changeToken body fallback removed (M2), backup path containment check (M3), typed editedData preview (M4), SMTP password encrypted at rest (M6), nginx security headers on static assets (M1/M8), dotfile access blocked, `Math.random()` replaced with `crypto.randomBytes()` (L1), setup rate limited (L6), `parseInt` radix (L5), RFC 5987 Content-Disposition encoding (L7). ### Added - **Parser system marked as beta** — Navigation and parser page show a beta badge and notice explaining that only LLM-based parsing is fully tested. - **SECURITY.md** — Comprehensive security architecture documentation with verification commands, audit history, and vulnerability reporting via GitHub Private Security Advisories. ### Changed - **Multi-flight parser roadmap** — V1.8 roadmap updated with two-stage hybrid parser design (block splitting + per-block extraction) and multi-version template scoring. ## [0.23.1-beta] - 2026-04-13 ### Fixed - **Sidebar overflow on small screens** — The navigation menu, flight list, and stats sidebars used fixed pixel widths (w-80/w-72) that overflowed on narrow viewports. All three panels are now capped at calc(100vw - 3rem), leaving a 48px tap area to close the backdrop. Also fixed the mobile menu DOM nesting by moving it outside the header element into a fragment. ## [0.23.0-beta] - 2026-04-13 ### Added - **Smart API check scheduling** — Replaces fixed-interval polling with per-flight scheduling using a geometric midpoint approach. Checks become more frequent as departure nears (min 10 min), run every 15 min in-flight, and stop 2 hours after arrival. Existing scheduled flights are backfilled on startup. ### Fixed - **Aircraft name normalization** — Aircraft type names are now normalized consistently across statistics, achievements, and the input form. Existing names in the database are cleaned up on startup. ## [0.22.0-beta] - 2026-04-13 ### Fixed - **Timezone-aware statistics** — All airport timezone data is now derived from coordinates via geo-tz and backfilled on startup. Fixes timezoneHopper (was always 0), fastestRoute showing impossible speeds (e.g. 5560 km/h), and incorrect flight durations for cross-timezone flights. - **Airline name normalization** — Duplicate airline entries caused by different import-source spellings (EgyptAir/Egyptair, AIR CANADA, Vietnam Airline) are now merged in statistics. - **WebGL2 graceful fallback** — Browsers without WebGL2 support no longer crash deck.gl; the map renders correctly. - **i18n encoding fixes** — Corrected garbled German umlauts in error messages and added missing translation key. ### Changed - **Same-day flights stat renamed** — "Same-Day Returns" renamed to "Tagesflüge" / "Same-Day Flights" to accurately reflect the metric (flights landing on the same calendar day, not round trips). ### Added - **Native MapLibre route fallback** — WebGL1-only browsers now get a native MapLibre GeoJSON route layer instead of no map at all. ## [0.21.0-beta] - 2026-04-13 ### Added - **Historical flights** — Flights can now be recorded as route-only entries without departure/arrival times. Includes a form checkbox, year/month partial date picker, grey map arcs, and a "HISTORISCH" badge in the flight list. - **Duplicate flight dialog** — One-click duplication of any flight as outbound or return, with correct status and time fields. - **Airport autocomplete in review modal** — The FlightReviewModal (used for email/boarding pass imports) now has full airport autocomplete with IATA/ICAO priority matching. - **Airline and aircraft autocomplete** — All flight forms now offer suggestions from a merged list of ~150 airlines and ~90 aircraft types (static seed data + user's flight history). Custom entries are remembered for future sessions. - **Year/month picker in edit modal** — Historical flights can be edited with a year/month-only date picker instead of requiring a full date. - **Three UX improvements for flight forms** — Enhanced form usability across add, edit, and review workflows. ### Fixed - **Achievement leaderboard inflated** — The leaderboard was counting all tracked achievements (55) instead of only unlocked ones (34), showing 12,595 points instead of the correct 3,630. - **Recent achievements showed non-unlocked entries** — The /recent endpoint now filters to only actually unlocked achievements. - **deck.gl map crash on load** — Fixed a luma.gl "r is null" race condition by deferring the DeckGLOverlay until MapLibre's WebGL context is ready (onLoad gate). Updated maplibre-gl 5.19→5.23. - **FlightEditModal date handling** — Overhauled date fields, feature flags, and historical status switching in the edit modal. - **Duplicate flight payload errors** — Fixed null field coercion, missing times, and incorrect status when duplicating flights. - **Validation errors hidden** — Detailed Zod validation errors are now displayed in the flight form instead of a generic error message. - **Flight schema rejects null** — Callsign and aircraft fields now correctly accept null values. - **Airport search ranking** — Exact IATA/ICAO matches are now prioritized over partial name/city matches. ### Docs - **Post-V1 roadmap** — Added ROADMAP-POST-V1.md with detailed specs for V1.1 (Cruises module) through V2.0 (Multi-user platform). ## [0.20.0-beta] - 2026-04-12 ### Added - **Historical flights** — New flight status for route-only entries without departure/arrival times. Historical flights count toward distance, airport, and geographic achievements but are excluded from flight-time statistics. Includes a form checkbox, grey map arcs (thinner width), a "HISTORISCH" badge in the flight list, and nullable times across the full Prisma schema. ### Fixed - **Null-safe frontend types** — Made departureTime/arrivalTime nullable across 17 frontend components to prevent runtime crashes when displaying historical flights without timestamps. ## [0.19.0-beta] - 2026-04-12 ### Added - **Scheduled flight distinction** — Future flights are automatically marked as "scheduled", shown with cyan arcs on the map and a "GEPLANT" badge in the flight list. Scheduled flights are excluded from all statistics. - **Flight list sort selector** — The sidebar flight list now has sort buttons for date (asc/desc), route, airline, and status. - **Planner & Survivor achievements** — Five new achievements for planned flights (Wanderlust, Globetrotter Planner, Year Ahead) and cancelled flights (Survivor, Turbulence Veteran). - **Improved airport labels** — IATA labels on the map are slightly larger with a more visible dark background for better readability. ### Fixed - **Achievements page readability** — Locked achievements are no longer blurred; they show at reduced opacity with a lock icon centered-right in the card. All text and progress bars remain clearly readable. - **Achievements page scroll** — The survivor/planner categories at the bottom of the page are no longer cut off. ## [0.18.1-beta] - 2026-04-12 ### Fixed - **Achievement seeding on every server start** — Achievement definitions are now ensured on every backend startup (idempotent), not only during the Docker entrypoint init script. Fixes empty achievements after fresh deploys or database resets. - **Round-trip route popup** — Route popup now correctly shows both airports (e.g. "Munich Airport → Helsinki Vantaa") instead of showing the departure airport twice for round-trip routes. ## [0.18.0-beta] - 2026-04-12 ### Added - **Two-stage route/trip info popup** — Clicking a route arc on the map now shows a redesigned popup with full airport names, distance, average duration, airlines, and seat class. A "Route Details" / "Trip Details" button opens a rich sidebar view with route statistics and a chronological flight list (or numbered trip legs for trip-routes mode). ## [0.17.0-beta] - 2026-04-12 ### Added - **Ollama model selector** — The admin parser settings page now shows a dropdown of all models available on the Ollama server instead of a free-text field. A "Pull model" section lets admins download new models directly from the UI. - **Cost tracking feature gate** — The cost breakdown section (price, currency, taxes, fees) in the flight review modal is now hidden unless the enableCostTracking feature flag is active. ### Fixed - **Timezone-aware flight durations** — Flight duration calculations now account for timezone differences between departure and arrival airports using IANA timezone data. Fixes inflated durations for international flights (e.g. LAX→MUC showed 20h instead of ~11h). - **Achievement checks for all flights** — Achievement progress is now evaluated after every flight creation, not only for flights with status "flown". ## [0.16.1-beta] - 2026-04-12 ### Fixed - **Setup page simplified** — Removed privacy info box, tips section, and instance name field. Only the essential username + password form remains. - **Profile shows account username** — The settings profile now displays the actual account username instead of the default "Traveler". - **Language auto-detected from browser** — On first use, the UI language is auto-detected from `navigator.language` (de/en) instead of defaulting to English. - **Route click selects all flights** — Clicking a route arc on the map now highlights all flights on that route instead of only the last one. - **Flight list refreshes after batch import** — The map and sidebar now update automatically after importing multiple flights via email. - **Batch import errors shown as toast** — Import errors (e.g. rate limit) are now displayed as a visible toast notification instead of silently failing behind a closed modal. - **Batch rate limit increased** — Raised from 10 to 50 requests per hour to support bulk email import workflows without hitting 429 errors. - **Achievement notifications on batch import** — The batch endpoint now returns newly unlocked achievements, and the popup is shown after bulk imports (previously only worked for single-flight additions). ### Changed - **Cost tracking defaults to off** — Cost input fields are now opt-in via Settings > Features instead of being visible by default. ## [0.16.0-beta] - 2026-04-12 ### Security - **Full pentest with 32 findings resolved** — Comprehensive code review uncovered 4 critical, 10 high, 12 medium, and 6 low severity issues. All 32 have been fixed: admin data export no longer leaks password hashes or decrypted API keys; shell injection in backup restore replaced with safe `spawn()` calls; Bearer header fallback removed (JWT only via HttpOnly cookie); force-change-password token now delivered via HttpOnly cookie instead of response body; login timing oracle eliminated (constant-time bcrypt for unknown users); MAX_USERS hard limit enforced even with ALLOW_REGISTRATION=true; SSRF protection on Ollama URL; invitation registration wrapped in serializable transaction; SameSite upgraded to Strict; CORS dev bypass removed; DB password default removed from prod compose; rate limiters keyed by userId on authenticated endpoints; nginx security headers on static routes. Zero npm audit vulnerabilities in both backend and frontend. ### Added - **Default Ollama model switched to gemma3:12b** — Benchmarked at 100% accuracy across all test email samples, replacing qwen2.5:7b. ### Changed - **Dead LLM training system removed** — Deleted modelManager, trainingAuth middleware, training settings route, 6 Python scripts, and ~2 GB of PyTorch dependencies from the Docker image. Dead Prisma schema fields dropped via migration. - **Oversized files split** — regexParser.ts (824 to 338 lines + 4 modules), backupService.ts (1129 to 378 + 4 modules), statsCalculator.ts (886 to 17 barrel + 4 modules). All files now under the 800-line limit. ### Tests - **Backend test isolation improved** — 7 test suites refactored to seed users via Prisma instead of the register endpoint, run serially via `maxWorkers=1`, and wipe state before each test. ## [0.15.2-beta] - 2026-04-11 ### Fixed - **Password min length aligned with the backend on the invited register page** — The client-side check rejected passwords below 6 characters while the backend Zod schema already required at least 8, so 6- or 7-character passwords slipped past the client and hit the generic "Validation error" envelope from the backend error handler. The invited user saw a red box with no explanation. Fix: the client check is now 8 characters to match, the `register.passwordTooShort` string in both DE and EN says "mindestens 8" / "at least 8", and the error handler now prefers `details[0].message` (the actual Zod per-field message) over the generic envelope when the backend does return one. Verified locally against a fresh invitation: a 7-char password now surfaces the exact "at least 8" message client-side, a valid 10-char password completes the flow. ## [0.15.1-beta] - 2026-04-11 ### Fixed - **Copy-to-clipboard on insecure HTTP origins** — Every "Copy link" button in the admin area silently did nothing when TravStats was accessed over plain HTTP on a LAN IP (the typical deployment), because `navigator.clipboard.writeText` is only available in a secure context (HTTPS or `localhost`). A new shared `copyToClipboard` utility tries the modern API first and falls back to a hidden-textarea plus `document.execCommand("copy")` path when the modern API is unavailable or rejects. The fix is wired into the invitation success modal, the row-level copy action in the invitation list, and the admin temporary-password reveal. Verified locally by running the dev server on a LAN IP (`window.isSecureContext === false`, `navigator.clipboard === undefined`) — both copy buttons now surface the success toast and actually write to the clipboard. ## [0.15.0-beta] - 2026-04-11 ### Added - **Invitation system v2** — The admin invitation surface is rebuilt end-to-end. Two distinct entry points replace the browser `prompt()`: a "Create link" modal that generates a shareable URL with an expiration radio (24h / 7d / 30d), and a "Create by email" modal that additionally sends the invitation through SMTP to a given recipient. The success modal keeps the link visible with a copy button until the admin dismisses it, so the URL is never "lost in a toast" again. On SMTP failure the invitation is still created and the modal shows an amber warning with the concrete error and the fallback link. - **Row-level invitation management** — Every invitation row in the admin list now has context-aware actions: re-copy the link on any active invitation, resend the email (only for rows with an email attached and a null/failed send status), and revoke (hard-delete) any non-used invitation. A filter chip row above the table (`Alle` / `Aktiv` / `Verwendet` / `Abgelaufen`) narrows the view, and a new "Verwendet von" column shows which user consumed each used invitation. - **`MAX_USERS` enforcement at invitation create time** — Both create endpoints now run a serializable transaction that counts `users + active invitations` and rejects with `409 User limit reached` before any row is inserted. `MAX_USERS` was previously only a warning log that never actually blocked anything. - **Auto-populated `notificationEmail` on invited registrations** — When a user registers through an invitation that carried an email address, that address is copied into the new user's `notificationEmail` field during the same insert. Password reset works for invited users out of the box without a manual visit to the settings page. - **Email delivery tracking per invitation** — The `invitations` table gets three new columns (`email_status`, `email_error`, `email_sent_at`) so the list row can show the last send outcome and the resend action can target failed deliveries specifically. The `used_by` foreign key is tightened to `ON DELETE SET NULL` so deleting a user no longer cascades to historical invitation rows. - **Dedicated `sendInvitationEmail` helper** — A new function in `emailService.ts` mirrors the existing `sendPasswordResetEmail` pattern but throws on SMTP misconfiguration instead of silently returning. The route handler catches the throw, marks the invitation `email_status='failed'` with the underlying error text, and still returns 200 — the _invitation_ create succeeded even when the _email send_ did not. ## [0.14.1-beta] - 2026-04-11 ### Fixed - **Invitation system wired end-to-end** — The register flow never forwarded the invitation token to the backend, so invited users silently registered as normal uninvited users and the invitation record stayed forever "active" in the admin list. `authApi.register` now accepts a third `invitationToken` argument, `RegisterPage` reads the `?token=` query parameter via `useSearchParams`, and a green "you are registering with an invitation" banner confirms that the link was picked up. Companion prod-config change: `FRONTEND_URL` and `ALLOW_REGISTRATION=false` should be set in the production docker-compose.yml so invite URLs no longer point to localhost and public registration is actually gated on a valid invite. Verified end-to-end against a local dev instance: registration without a token is blocked, an admin-created invite flows through the register page, the `invitedBy` column is populated, and a second use of the same token is rejected by the database unique constraint. ## [0.14.0-beta] - 2026-04-11 ### Added - **Help texts on every settings and admin surface** — 10 components (SmtpManager, ParserSettings, ApiKeys, Backup, Defaults, Features, Map, Notifications, Profile, Units) now ship expandable InlineHelp blocks with DE + EN explanations covering scope, dependencies and gotchas. Coverage is now 27 components with 198 checked i18n keys. - **Help audit script** — `scripts/audit-inline-help.mjs` walks every InlineHelp and HelpIcon element and verifies each `t()` key against DE and EN. Exits non-zero on missing translations; usable as a CI gate. ### Fixed - **Backup creation via the UI** — The route handler passed pre-computed paths to the service through `existingRecord`, but the service generated a fresh timestamp internally and therefore ran `mkdirSync` on a different directory than it tried to write to. Result: `pg_dump > .../temp/database.sql: Directory nonexistent`. The service now takes `backupDir`/`tempDir` directly from `existingRecord`. - **"Date unknown" in the backup table** — `serializeBigInt()` fell into the `typeof === 'object'` branch for Date objects and turned them into `{}` because `Object.entries(date) === []`. Fix: non-plain objects are passed through unchanged so Express can call `Date.toJSON()` for wire serialization. +7 regression tests. - **Consistent airline column** — Some flights showed "Lufthansa", others just "LH". A new `resolveAirlineDisplay()` helper expands 2-char IATA codes to the full airline name; applied everywhere (FlightsTablePage, FlightList, FlightCalendar, FlightSelectStep, YearHeatmap). - **Trip filter chips** — The buttons rendered the raw i18n keys `filter.with` / `filter.without` because the JSON held `withTrip` / `withoutTrip`. Keys renamed in both DE and EN to match the code. - **Missing lib files committed** — `airlineUtils.ts` and `filterEmailText.ts` were imported by `FlightReviewModal` and `EmailAnnotation` but only existed locally (untracked). A clean clone would have failed to build. - **White input fields on auth pages** — LoginPage, ForceChangePasswordPage, ResetPasswordPage and AdminPasswordResetModal referenced a CSS class `.input-field` that does not exist anywhere, so the inputs fell back to the browser default (white). Switched to the existing `.input` class. - **"Note" label in developer mode help** — The key `settings:developer.help.noteLabel` was missing in both languages, so the UI rendered the raw i18n key when the help was expanded. Added in DE + EN. ## [0.13.0-beta] - 2026-04-06 ### Added - **Email-based password reset** — Users can request a reset link from the login page via "Forgot password?". The link is delivered by email (only when SMTP is configured). - **Admin password reset** — Admins can reset a user's password from user management: either generate a random temporary password (shown once, with a copy button) or set a password directly. Optionally, a "Must change password on next login" flag can be enabled. - **Forced password change** — When the admin sets the flag, the user must choose a new password on the next login before they can access the app. - **Rate limiting on reset endpoints** — Password reset requests are limited to 5 requests per 15 minutes to prevent abuse. ### Fixed - **Force-change-password route** — After login the frontend incorrectly called `/force-change-password` instead of `/change-password`; fixed. - **Unique constraint on token fields** — Reset and change tokens now have a database unique index that enforces single-use semantics at the DB level. ## [0.12.2-beta] - 2026-04-06 ### Security - **nginx version leak fixed** — `server_tokens off` in the nginx config prevents exposure of the nginx version in response headers. - **Duplicate security headers removed** — nginx no longer sets security headers; Helmet owns them completely, avoiding conflicts on X-XSS-Protection, Referrer-Policy and HSTS. - **XSS sanitization in flight notes** — HTML tags are stripped from the backend `notes` field before being stored. ### Fixed - **Express 404 pages** — Missing routes now return `{"error":"Not found"}` instead of the internal Express HTML page. - **JSON parse errors anonymized** — Invalid JSON bodies now return a generic error message instead of the internal parser error text. ## [0.12.1-beta] - 2026-04-06 ### Security - **CORS tightened** — The CORS wildcard now only applies in development mode (`NODE_ENV === development`), not in all non-production environments. - **Additional rate limiting** — Upload endpoint (30/hr) and every settings route (60/15min) now have rate limits guarding against disk exhaustion and enumeration. - **Minimum password length raised** — Minimum length raised from 6 to 8 characters. ### Fixed - **Database start failures surfaced immediately** — Missing DB configuration now aborts server startup instead of just warning. - **Global error handlers** — Unhandled promise rejections and uncaught exceptions from schedulers are now logged instead of silently crashing the server. - **Parser settings endpoint corrected** — PUT `/api/v1/settings/parser` incorrectly returned 200 OK without saving anything; now correctly returns 501 Not Implemented. - **Query parameter validation** — `parseInt()` in admin parse-log routes replaced with Zod schemas (prevents NaN reaching DB queries). - **Trip list query bounded** — GET `/trips` now loads at most 500 trips - 200 flights per trip (was: unbounded). - **ErrorBoundary logging** — `console.error` in ErrorBoundary is now scoped to development mode. - **package.json versioned** — `backend/package.json` and `frontend/package.json` synchronized to `0.12.1-beta`. ## [0.12.0-beta] - 2026-04-06 ### Added - **Empty map view** — New users now see a hint card with a direct "Add flight" button instead of an empty globe. - **ICAO code in the airport tooltip** — Clicking an airport label now additionally shows the ICAO code as a badge next to the IATA code. - **Gate, terminal, boarding group and companions in the edit modal** — These four fields are now fully editable in the flight edit dialog. ### Fixed - **Language support** — Hardcoded German strings in the map tooltip, trips tab and flight counter replaced with i18n keys; dead ContextualHint reference removed. All text now reacts correctly to the language setting. - **Umlaut bug** — `¨e` (a Prettier artifact) in the plural "Flüge" fixed. - **Exit highlight mode** — Clicking on empty map space now reliably ends trip or flight highlight. ### Changed - **Tooltip performance** — `onMove` handler throttled via requestAnimationFrame; tooltip recalculation runs at most once per frame instead of up to 60 times per second. - **Code quality** — Plane and pulse animations extracted from DeckGLMap into dedicated hooks (DeckGLMap: 582 → 430 lines); TooltipContainer and formatDuration provided as shared primitives. ## [0.11.0-beta] - 2026-04-06 ### Added - **Trips tab in the sidebar** — Quick selection of all saved trips directly from the flight panel, including flight count, year and total kilometers. - **TripTooltip on the map** — Selecting a trip now shows an info card with the route chain, travel dates, total duration, distance, airlines and aircraft types. - **Airport statistics tooltip** — Clicking an IATA code or airport point opens a stats card with departures/arrivals, most common routes, total kilometers and operating airlines for that airport. - **Auto-highlight when using "Show on map"** — All flights of a trip are automatically selected as soon as the user switches into the trip route view via "Show on map". - **Airport markers on the trip route layer** — Pulsed rings and labels for all departure and destination airports of the active trip. - **5 demo trips** — Seed data for new users now includes five predefined trips (Barcelona, Dubai & Singapore, Japan, Scandinavia, USA West Coast). ### Fixed - **Tooltips follow the map** — All info windows (flight, trip, airport) update their position while scrolling and zooming through geo-anchored projection. - **Trip info card above the arcs** — Tooltip is positioned above the bounding box of all airports so it does not sit on top of the route lines. - **Sidebar stays open** — Selecting a trip from the sidebar panel no longer auto-closes the panel. - **Back to the normal view** — Closing the trip tooltip resets `visMode` to "routes" and clears the trip selection. - **Arc click tolerance** — `pickingRadius: 5` on the MapboxOverlay fixes the issue where clicks on thin arcs were not registered. - **deck.gl layer re-rendering** — Colors are pre-computed in the data so deck.gl reliably repaints on selection. Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html) ## [Unreleased] --- ## [0.10.0-beta] - 2026-04-05 (Update 2) ### Added - **`operatingAirline` field**: New optional DB field `operating_airline` (migration `20260405000000_add_operating_airline`). Stores the operating carrier for codeshare flights as well as rail/bus tickets. - **"Operated by" form field**: Added to `FlightEditModal` and `FlightCompleteStep` with `` autocomplete. - **Airline autocomplete**: All airline fields now have browser-native `` autocomplete with ~75 airlines including rail/bus operators (DB, FlixTrain, Flixbus, ÖBB, SBB, TGV, Eurostar). - **`cleanEmailBody()` utility**: Cleans up plain-text email bodies before parsing — strips HTML tags, URLs, normalizes whitespace. Mirrors the `filterEmailText` function used in the annotation view. ### Changed - **Parser runs against cleaned text**: `parseEmail()` applies `cleanEmailBody()` before handing text to any parser — parser and annotation view now see identical text. - **`EmailAnnotation` stores filtered text**: `fullText` on annotations is stored as `filterEmailText(raw)` instead of raw — annotation positions and pattern derivation are consistent. - **Airline auto-derived**: Post-processing derives the airline name from the flight number prefix (`LH2316` → `LH` → `"Lufthansa"`) when `airline` is empty. - **Parser recognizes "operated by"**: Patterns like `"operated by X"` / `"durchgeführt von X"` are automatically captured as `operatingAirline`. - **`AIRLINE_IATA_MAP`** expanded from 15 to ~75 airlines. ### Fixed - **400 error when adding a flight**: The Zod schema threw when `airline: ""` (empty string) was submitted. Fix: `emptyStringToUndefined` transform on `airline` and `flightNumber` in `baseFlightSchema`. - **LH-old template**: `flightNumber` pattern changed to `^([A-Z]{2}\s+\d{3,4})$` — works after `cleanEmailBody()` because `\tLH 2316\t` becomes `LH 2316` on its own line. Template version bumped to `2025-04b` and pushed to the GitHub templates repository. --- ## [0.10.0-beta] - 2026-04-04 ### Added - **Enrichment two-mode system**: flights < 1 year → full enrichment (aircraft, ICAO codes, route, terminal, gate); flights ≥ 1 year → slim enrichment (ICAO codes + terminal only). - **`getEnrichmentMode()`**: Exported helper that determines enrichment mode from flight age. - **Route median interpolation**: `aggregateRoutes` now resamples all reference routes to 20 points and computes per-position median lat/lon — replaces the "take newest route" approach. - **Enrichment badge in PendingUpdateCard**: Full (green) / slim (amber) badge plus an "estimated · not verified" disclaimer with reference flight count and confidence score. - **Two-mode explanation panel** in Settings → Enrichment tab. ### Changed - **`findEnrichmentCandidates`**: Now excludes flights with `pending` or `rejected` pending updates (previously only `applied` was excluded — caused infinite nightly re-processing). - **Settings simplified**: `requireApproval` and `autoProcess` removed — enrichments always create a pending update requiring manual confirmation. Settings reduced from 6 to 3 fields (`enabled`, `minConfidence`, `maxPerDay`). - **Scheduler**: Runs for all users with `historicalEnrichmentEnabled=true` (no longer gated on `autoProcess`). ### Removed - **`historicalEnrichmentMaxAgeYears`**, **`historicalEnrichmentAutoProcess`**, **`historicalEnrichmentRequireApproval`** from the `UserSettings` schema and DB. --- ## [0.9.6-beta] - 2026-04-03 ### Added - **Ollama config in admin UI**: `ollamaUrl`, `ollamaModel`, `ollamaVisionModel` stored in the `AdminSettings` DB table; editable in Admin → Parser Settings. - **Backup schedule in admin UI**: `backupEnabled`, `backupInterval`, `backupRetentionDays` stored in `AdminSettings`; editable in Admin → Backup Management. - **`dateUtils.ts`**: Timezone-aware date/time formatting with `Intl.DateTimeFormat`; graceful UTC fallback for invalid timezone strings. - **Timezone-aware flights table**: `FlightsTablePage` now formats dates using the user's configured timezone from the settings store. ### Changed - **`getParserConfig()`**: Reads `ollamaUrl/Model/VisionModel` from the `adminSettings` parameter with ENV fallback. - **`backupScheduler.ts`**: `getBackupSettings()` reads backup config from the DB instead of ENV; runtime validation via `VALID_INTERVALS` / `toBackupInterval()`. - **`BackupSection`** (Settings): Now a read-only status view; backup schedule configuration moved to Admin → Backup Management. - **`NotificationsSection`** (Settings): Removed dead toggles; now renders only `NotificationPreferences`. ### Removed - **`debugLoggingEnabled`** + **`requireUserApiKeys`** from `AdminSettings` schema — log level from ENV, API keys always required. - **`trainingSeparateModels`** from the `UserSettings` schema. - **`SystemSettings`** model dropped entirely. - **Ghost backup/notification fields** from `settingsStore`, `useSettingsPage`, API types. --- ## [0.9.5-beta] - 2026-04-03 ### Removed - **LLM training dead code**: Deleted orphaned `TrainingDashboard`, `TrainingDataFilters`, `TrainingDataPreview` components (never imported after the TrainingPage → ParserPage refactor). - **Save + Train button**: Removed the LoRA fine-tuning trigger from `EmailAnnotation` and `BoardingPassAnnotation` — annotation now always derives templates via the `annotate` endpoint only. - **Dead `trainingApi` methods**: `saveAndTrain`, `trainOnly`, `getData`, `getJobs`, `getJobLogs`, `triggerTraining`, `cancelTraining`, `deleteTrainingData` removed from the frontend API client. - **Dead TypeScript types**: `TrainingJob`, `TrainingJobLog`, `TrainingJobLogsResponse` removed from `types/index.ts`. - **LLM-only backend endpoints**: `POST /:id/save-and-train`, `POST /:id/train-only`, `GET /data`, `GET /jobs`, `GET /jobs/:id/logs`, `DELETE /:id`, `POST /trigger`, `POST /jobs/:id/cancel`, `GET /data/analysis` removed from the training route. --- ## [0.9.3-beta] - 2026-04-03 ### Added - **Annotation-driven template parser**: Users can annotate parsed email fields to derive regex-based templates. Annotated patterns are stored as `ParserTemplate` records and applied as step 0 in the parser factory for future emails from the same airline. - **TemplateDeriver**: Derives multi-flight regex templates from user annotations (`textSelections`) with field source tracking (`fieldSources`). - **FingerprintMatcher**: Matches incoming emails to existing user templates by airline/subject fingerprint. - **UserTemplateEngine**: Executes derived templates against email bodies with multi-flight extraction. - **Parser templates CRUD API**: `GET/POST/DELETE /api/v1/parser-templates` — list, activate, disable, delete user-derived templates. - **TemplateReviewCard**: UI card shown after annotation save to display the newly derived template with confidence score. - **Colour-coded confidence borders** in `FlightReviewModal`: green (template match), yellow (LLM fallback). - **`fieldSources`** on `ParsedBooking` — tracks which field was extracted by which method. ### Fixed - Regex PNR `matchAll` missing `g` flag — caused 500 errors during email parsing. - `GET /api/v1/parser-templates/:id` endpoint added (was missing). - Body length guard against ReDoS attacks in parser template routes. - `TemplateReviewCard` async error handling and loading states. --- ## [0.9.2-beta] - 2026-04-02 ### Changed - **Map amber redesign**: The glassmorphism theme now uses TravStats brand colors (amber → orange → red) throughout all visualization modes. Replaces the previous indigo/cyan color scheme. - **Filter as FAB**: Filter button moved from the bottom-center bar to a bottom-right FAB stack (frosted-glass style, opens upward). The mode FAB is stacked above the filter. - **CSS tokens**: `--map-accent`, `--map-fab-gradient`, `--map-active-*`, `--map-badge-*` all updated to amber. Sepia CSS filter on the map canvas removed. Dark-matter map style restored unconditionally. ### Added - **Globe night earth**: Night-earth texture (`earth-night.jpg`) with amber atmosphere glow + starfield background (`night-sky.png`) in globe mode. - **Globe legend stacking**: Auto-rotation toggle and route-frequency legend share the bottom-left column (no overlap). ### Fixed - Airport labels (`TextLayer`) now always render above arc lines (`depthCompare: "always"`). --- ## [0.9.1-beta] - 2026-03-31 ### Added - **Email import as the primary tab**: Email import promoted to the main "Import" tab in the UI. `EmailImportTab` component with drag & drop, airline notice and text fallback. - **Template status view**: Settings page shows GitHub-linked template status. New `TemplateStatusView` component + `/api/v1/templates/status` endpoint. - **Duplicate flight detection**: POST `/api/v1/flights` returns 409 with `existingFlight` details when a duplicate is detected. The frontend shows a confirmation dialog with an "Add anyway" option (`?force=true` bypass). - **Year-over-year statistics**: `/api/v1/stats/summary?year=YYYY&compareYear=YYYY` with delta badges (↑↓ % change). - **Travel companions**: `companions` field on the Flight model. Tag-style input in `SimplifiedFlightFormV2`. - **Seat statistics**: `GET /api/v1/stats/seats` — distribution by position, zone and cabin class. - **Flight certificate**: `FlightCertificate.tsx` generates a downloadable PNG stats card via html2canvas. - **Email notifications**: SMTP config, per-user notification preferences, node-cron reminder scheduler. - `statsLimiter` (30 req/min) and `adminExportLimiter` (5/hr) rate limiters. ### Changed - **deck.gl visualization**: 6 map modes — routes, heatmap, hexagon (3D), 3D columns, trips (animated), globe (react-globe.gl). - `VisModeSelector`, `TimeSlider`, layer factories for all visualization modes. - Map integration refactored from Leaflet to deck.gl 9.x + MapLibre GL 5.x. - `flightNumber` now included in `calculateChanges` comparison fields (was silently ignored before). ### Fixed - Fresh-DB migration ordering: early migrations (`202501xx`) wrapped in `IF EXISTS` guards; catch-up migration (`20251221`) recovers all columns. - Backend CI: ESLint added as an explicit dependency, ESLint 9 rule violations resolved. - All 156 backend tests now pass (assertions aligned with actual service/route return types). - GeoJSON layer factories now read from `geometry.coordinates`, not unpopulated airport `lat`/`lon` fields. --- ## [0.9.0-beta] - 2026-02-24 First public beta release. Re-versioned from 1.0.x to 0.9.0-beta to reflect that not all planned features are complete yet. ### Added - Version badge in About tab (reads from package.json). - Rate limiting on backup-restore (3/hr) and training-trigger (2/hr) endpoints. - Zod validation for stats route query parameters. - PayPal donation and GitHub Star buttons in Settings → About. - i18n translations for all hardcoded strings in `SimplifiedFlightForm`. - Missing `unknownDate` i18n key in dashboard translations. - i18n translations for hardcoded strings in `DashboardPage` (PDF export, map/stats fallbacks). ### Changed - Package versions bumped to `0.9.0-beta` (frontend + backend). - Admin page refactored to sidebar layout. - All plain browser checkboxes replaced with the styled `.checkbox` class. ### Fixed - `alert()` calls in AchievementsPage replaced with toast notifications. - `console.debug()` calls in `barcodeExtractor.ts` replaced with `logger.debug()`. - Dark mode issues across all pages (hardcoded Tailwind colors → CSS variables). --- ## [1.0.1] - 2026-02-23 ### Added - Prettier formatter for frontend TypeScript/TSX/CSS (`format` and `format:check` scripts). - ts-prune dead code detection scripts in frontend and backend (`dead-code` script). - Vitest coverage reporting with v8 provider and regression thresholds (`test:coverage` script). - ruff.toml Python linter config; auto-fixed 220 issues in training scripts. - bandit security config (`.bandit.yml`) for Python script scanning. - License whitelist (`LICENSE_WHITELIST.txt`) covering all project dependencies. - Pre-commit hooks: trailing whitespace, YAML/JSON validation, secret detection, ruff, Prettier. - GitHub Actions CI workflow: backend (typecheck + lint + test with Postgres), frontend (typecheck + lint + format + coverage), Python (ruff + bandit). - Dev setup scripts (`scripts/setup-dev.sh` and `scripts/setup-dev.ps1`) for onboarding. - Smoke test script (`scripts/smoke-test.sh`) for post-deploy verification. ### Changed - Docker security hardening: `cap_drop: ALL` + minimal `cap_add`, `no-new-privileges`, log rotation, and resource limits on `app`, `db`, and `ollama` services. ### Fixed - Removed unnecessary `CAP_SETUID`/`CAP_SETGID` from the app container (root can setuid without them). - Corrected the pre-commit prettier hook entry to properly forward filenames (`npx --prefix frontend prettier --write`). - Removed dead coverage config block from `vite.config.ts` (shadowed by `vitest.config.ts`). ## [1.0.0] - 2026-02-23 ### Added - Initial stable release. - Flight tracking with map visualization (Leaflet, 3D globe). - Statistics dashboard (distance, time, routes, heatmaps). - Achievements & gamification system (20+ badges). - Boarding pass scanner (QR/barcode + OCR via Tesseract.js). - Email booking import (manual upload + IMAP polling). - Flight data lookup (AirLabs API integration). - OpenFlights airport database (~14,000 airports). - Export: CSV, GeoJSON, KML. - Tags & categories (business/private). - Cost tracking per flight. - Dark/light mode. - Multi-language support (DE/EN). - LLM-powered parsing (Ollama integration). - LoRA fine-tuning pipeline for email/boarding-pass models. - Pre-training data quality analysis (`checkTrainingData.py`). - Post-training model evaluation (`evalModel.py`). - Training metrics parsing (loss, steps, epochs). - Docker deployment with nginx + supervisor. - JWT authentication with secure cookie handling.