Skip to content

Trips: a page per journey, with its map, timeline and highlights - #98

Merged
blike merged 15 commits into
mainfrom
trips
Nov 8, 2026
Merged

blike merged 15 commits into
mainfrom
trips

Conversation

@blike

@blike blike commented Nov 8, 2026 •

Copy link
Copy Markdown
Owner

Adds trips: a page per journey with its totals, a map colored by day, a replay, a timeline of its drives, stops, nights and charges, and highlights below. A trip is its dates, so drives and charges join it on their own.

Trips on the server

  • A trip is its dates. Finished drives and charges that start within its days join it on their own, counted in the time zone they were set in. With no end date it takes in everything after its start.
    • One taken out stays out. An activity is in one trip at a time: adding one that's in another is refused unless asked to move it.
    • Two trips can't share a day. A trip without dates holds only what's put in it.
  • Storage: trips read the existing records, Rivian's or TeslaMate's, and store only which records each holds. Migration 0021 adds trips, trip_activities and trip_exclusions.
  • Totals (distance, driving and charging time, energy, efficiency, cost per currency) say how many drives or charges each figure covers.
  • Routes are thinned like History's, with each drive's elevation by distance taken from every reading, so a pass keeps its height.
  • TeslaMate says nothing when a drive ends, so trips take in what's finished when they're read, at most every 30 seconds per vehicle, and straight away after any change to a trip.
  • Discarding a charge or replacing records with an import takes them out of their trips. The demo scene makes a trip of its latest drives and charges.

Trips in the web app

  • Trips page: any trip under way first with the day it's on, then those to come, then those done, each a card with its dates as a calendar page and its figures.
  • A trip's page:
    • Totals, and a map colored by day with numbered stops, where a road driven more than once is drawn once, plus a replay of the journey.
    • A timeline of drives, stops, nights and charges. A drive opens with its battery from start to end and its average moving speed. A charge opens with its range added and its average and peak power.
    • Day by day, then highlights: lowest to highest elevation, climb and descent, longest drive, fastest charge, lowest battery and where, cost per mile.
    • Battery and elevation along the whole way, where it stayed, where the time went and who drove.
  • Making and editing: pick dates on the calendar Drives and Charging use, tinted with what was driven each day. Pick the day it set out and the end follows the way home, or leave it running. What started in its days comes ticked, to untick what wasn't part of it.
  • A drive or charge can be added to a trip from its own page, stretching the trip's dates to take it in, or to a new trip of its day.
  • Gaps: where one drive doesn't end where the next starts, the timeline says the driving between isn't in the trip, with a way to find it, and the map and charts bridge it with a dashed line.
  • A drive or charge removed from the timeline can be put back for a few seconds.

Also

  • Named destinations: a drive that ended where navigation was headed shows that place by navigation's name ("Jumbo Rocks Campground") rather than the road it's on. A saved location's name still comes first.
  • Driver ids: a driver lookup no longer uses up an id on every profile report (the oddity noted in Let users name a driver (and reassign a drive's driver) #91). Ids already spent stay spent.
  • Focus: keyboard focus is the accent outline everywhere, not the browser's blue.
  • Shared styles: secondary and danger buttons and text fields are .btn-secondary, .btn-danger and .field. plural and browserTimeZone move to lib/format.
  • The README describes trips.

Testing

  • Server: 672 tests pass, including the database tests against a throwaway Postgres. New tests cover trip membership, exclusions and moves, totals, route profiles, and navigated place names.
  • Web: 387 tests pass. Typecheck, lint and formatting are clean.

blike and others added 15 commits November 4, 2026 18:21
The writer used a navigation fix only while it was under a minute old by
the server's clock, and ignored every GNSS fix meanwhile. That dropped
navigation fixes delivered late, and GNSS fixes for up to a minute after
navigation stopped.

Now every navigation fix is stored at its own time, and a GNSS fix is
stored unless a navigation fix is within 15 s of it. On an R1S the
navigation stream ran at 5.0 s (p99 5.1 s), so 15 s keeps the feeds from
interleaving while letting GNSS take over as soon as navigation stops.
A late fix no longer becomes the point later moves are measured from.

Legacy now counts as a track of its own while it has three distinct
fixes within 30 s, instead of two short gaps followed by up to five
minutes of quiet. Replayed over 33 navigated R1S drives, the old rule
held navigation fixes back for 2,445 s after legacy had gone quiet; this
one never does, and a wake-up burst on an R2 holds for 30 s at most.
The held navigation fix is no longer deleted when legacy takes over.
The import flow and panel each kept their own copy of both. They now
live in lib/format, where the rest of the app can use them too.
The secondary and danger buttons were class strings kept in settings and
imported from there by shared components, and the text field's classes
were copied into each form. They're now .btn-secondary, .btn-danger and
.field beside .btn-primary, built from the same classes, so they look as
before. In the components layer, a caller's own utilities still win.

Three notification settings fields used the browser's focus style; they
now take the field's grey focus border like the rest.
Tabbing through the app showed the browser's blue focus ring, which on a
nav link also ringed the hidden current-page bar off its left edge. Focus
is now the accent outline that buttons and pickers already used, solid,
so it rings the element alone.

A focused sidebar link lights like the current page, with a faint accent
ring inside its edge rather than an outline crowding the links around it.
The sign-in field's focus is the grey border other fields use, not blue.
A driver was looked up by inserting its profile with ON CONFLICT DO
UPDATE, which takes the next id before finding the row is already there,
so every active-profile report, settled name and assigned drive used one
up: two drivers could sit at ids 1 and 631. The row is now updated first,
and only a profile seen for the first time is inserted, with the upsert
kept for two arriving at once. The last-seen time still only moves
forward.

This is the id oddity noted in #91; the ids already spent stay spent.
A drive that ended where navigation was headed now shows that place by
the name navigation gave it, "Jumbo Rocks Campground" rather than the
road it's on, wherever the vehicle parks there, ahead of the looked-up
address. A saved location's name still comes first.

Parked within half a kilometre of the destination counts as arriving,
or two and a half where the address has no house number (a park, a
campground), whose pin can sit well away from the car park. A name
that's only an address is left to the looked-up one, and a charger's
"Joshua Tree, CA [Rivian Adventure Network]" reads "Rivian Adventure
Network · Joshua Tree".
A trip is its dates. Finished drives and charges that start within its
days, counted in the time zone they were set in, join it on their own,
and with no end date it takes in everything after its start. One taken
out stays out, and an activity is in one trip at a time: adding one
that's in another is refused unless asked to move it. Two trips can't
share a day. A trip without dates holds only what's put in it.

Trips read the existing records, Rivian's or TeslaMate's, and store only
which records each holds. Their totals (distance, driving and charging
time, energy, efficiency, and cost per currency) say how many drives or
charges each figure covers. A trip's routes are thinned like History's,
with each drive's elevation by distance taken from every reading, so a
pass keeps its height.

TeslaMate says nothing when a drive ends, so trips take in what's
finished when they're read, at most every 30 seconds for each vehicle,
and straight away after any change to a trip. Only activities not in a
trip already are offered, so ids aren't spent on every read.

Discarding a charge or replacing records with an import takes them out
of their trips. Drive and session listings are built by shared helpers
the trip routes use too. The demo scene makes a trip of its latest
drives and charges.
A Trips page lists the vehicle's trips, any under way first with the day
it's on, then those to come, then those done, each as a card with its
dates as a calendar page and its figures. A trip's page has its totals,
a map colored by day with numbered stops, where a road driven more than
once is drawn once, and a replay of the journey, beside a timeline of its
drives, stops, nights and charges. Below come highlights, the battery and
elevation along the whole way, each day, where it stayed, where the time
went and who drove.

A trip is made or edited by its dates, picked on the calendar Drives and
Charging use, tinted with what was driven each day. Pick the day it set
out and the end follows the way home, or leave it running with no end;
what started in its days comes ticked, to untick what wasn't part of it.
A drive or charge can be added to a trip from its own page, stretching
the trip's dates to take it in, or to a new trip of its day.

Where one drive doesn't end where the next starts, the timeline says the
driving between isn't in the trip, with a way to find it, rather than
calling it a stop, and the map and charts bridge it with a dashed line.
A drive or charge removed from the timeline can be put back for a few
seconds. A charge counts on the day it charged, as on Charging.

The Drives and Charging calendar's popover is shared with the trip's
date fields, a card that's a link warms its edge on hover everywhere,
map markers take their dark face from a token, and the README describes
trips.
Legacy counted as a dense track when the navigation fix was within 30 s
of the oldest of its last three fixes. On a feed reporting once a
minute those three span two minutes, so a navigation fix stamped before
the newest legacy fix read as dense and was dropped. Now only the legacy
fixes at or before the navigation fix count, and three of them must fall
in the 30 s up to it.

The feed is judged only when a delta brings a new navigation fix. The
cached one was settled when it arrived; judging it again as it aged
logged a switch of source that never happened.

A navigation fix refused for stepping the track back no longer claims
its 15 s, so it can't mute the GNSS fix that is the only point there.

The replay test now uses a new monitor, so only the stored row stops the
frame being stored twice, and checks the save doesn't fail.

Co-authored-by: Grant Leisz <MrSerious0@users.noreply.github.com>
An R2 reports its own fix about once a minute, so its marker on the
dashboard jumped once a minute even while navigation was sending a fix
every 5 s. The map now follows the rule the stored track does: the
navigation fix while one is running, and the vehicle's own fix once that
is more than 15 s newer. Speed and heading come with the fix shown, so
they match the marker. An R1, whose own fix is already a track, is
unchanged, since the server doesn't keep its navigation fixes.

A delta carrying only a navigation fix now reaches the live stream too,
so the map moves as each one arrives. The live trail skips a fix older
than the last one drawn, so it doesn't step back when navigation's fix
was taken a moment before the GPS fix already shown. The out-of-date
warning judges the fix the map shows.

Geofences, charging and drive detection still read only the vehicle's
own fix. The 15 s window moves to state-utils so the web shares it.
A drive on the trip's timeline now opens with its battery from start to
end, "82% → 78%", and its average speed while moving, where its route
can tell moving from stopped. A charge opens with the range it added and
its average and peak power, worked out from energy and time where no
average was reported.

Elevation among the trip's highlights is now lowest to highest rather
than the highest point alone, with the descent beneath the total climb,
and the elevation chart says how far it came down too. "Closest call" is
now "Lowest battery", with where the drive that left it there ended; the
weather swing is gone, as a few lookups can't vouch for it.

Day by day comes before the highlights, which are smaller, in as many
columns as fit. The replay button is a small pill until it plays, then
widens for the drive under way and the scrubber, keeping its height.
… across

Rivian Roamer is now the first source under Settings → Import. Its data
copy is one .zip, read in place, and matched to your Rivian by VIN.

From a Rivian Roamer copy:
- Drives and charges are Roamer's own sessions; ones it flagged as
  corrupted are left out.
- Charges keep their energy breakdown and a curve from Roamer's charging
  readings. A charge you already have only gets what it's missing, and
  undo puts it back as it was.
- Vehicle history comes from the metrics Roamer kept as Rivian sent them;
  ones it recoded (seat heat, service mode, range thresholds) are skipped.
- Each drive gets the driver profile and navigation destination Roamer
  recorded, and the account's saved places are added unless one is
  already saved there.

ElectraFi drives now get their driver too: ElectraFi keeps the active
profile's name in Tesla's autopark_state_v2 column.

Drivers are matched by name to ones RivianMate knows. A name it doesn't
know becomes a driver without a profile, which the car's profile takes
over once it reports that name. Undo keeps drivers and places.

The import itself:
- The shared steps (drafts, planning, writing, undo) move to
  services/imports, with each export as a format that reads its files.
- The preview counts readings left for live tracking apart from ones
  already recorded, and lists the places it adds.
- The flow's rows hold their size as files and dates load, and its text
  is shorter.
The show-password toggle sat between the two fields when choosing an
app password, so Tab went to it before the confirm field.
# Conflicts:
#	web/src/features/drives/DrivesPage.tsx
Merges main, track-fix-precedence (#95) and roamer-import (#97) into trips so all three ship in one merge to main, and one Docker Hub publish.

Trips now uses the import code where Roamer moved it: isTimeZone from imports/time-zone and rangeOf from imports/plan. The import flow uses Trips' shared btn-secondary and btn-danger classes and the plural from lib/format.
@blike
blike merged commit 9e2baec into main Nov 8, 2026
1 check passed
@blike
blike deleted the trips branch November 10, 2026 14:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant