Logbook, trips list, upload and boats¶
| Rails | |
|---|---|
| Status | ✅ |
| Code | app/controllers/{logbooks,trips,boats}_controller.rb, app/services/{logbook_stats,trip_filter,fit_upload}.rb, app/helpers/logbook_helper.rb, app/views/{logbooks,trips,boats,stats}/, app/views/profiles/_cover.html.erb, app/assets/stylesheets/components/stats.css, app/javascript/controllers/globe_controller.js, public/geo/land-110m.json, app/views/components/{_header_account,_site_header,_split_bar}.html.erb, app/javascript/controllers/{upload,mini_map,auto_submit,filters_toggle,inline_boat}_controller.js, config/locales/{app,logbook}.en.yml |
Verified against code: 2026-10-22
Purpose¶
The signed-in area under /my is the user's logbook: everything sailed, added up, with every trip, the boats sailed on and the paired devices. Tracks arrive from paired devices by themselves or through the upload dialog. Only the signed-in owner sees any of it.
Formats (NM, durations, dates) follow README.md § Shared formats. Numbers come from the trip aggregates (ingestion.md § Trip aggregates).
Signed-in navigation¶
Application layout, header in app mode.
| Element | Content |
|---|---|
| Logo | → / (public home) |
| Links | Logbook (/my), Trips (/my/trips), Boats (/my/boats), Devices (/my/devices), each with an icon. Trips stays active on trip, track and day pages. |
| Account menu (desktop) | Button with user icon and the first name (or "Account"). Opens a popover below it, right-aligned, as tall as its content (rows packed at the top, scrolls past the window height): the email address, "Profile" (user icon), "Account" (lock icon), "Subscription" (star icon), "Admin" (admins only, → /admin, admin.md), "Sign out". |
Mobile web¶
Two icons on the right: Menu opens the four links; Account opens the same items as the account menu (README.md § Mobile web).
Logbook (/my/logbook)¶
/my forwards here (302), with the locale prefix kept.
Skipper header (PRO)¶
The skipper header instead of a page head: header image (4:1) or a dark band, the round avatar or the initial, the title "/my/account#public-profile), and the sailor bio when written. No Upload button here (it is on the trips list), no "Your numbers" heading: the totals follow the header and banners.
Page head (standard users)¶
Title "Logbook", lede "Everything you sailed, added up.", button Upload tracks (Upload dialog). Only without PRO; today every user is PRO.
Banners¶
| Banner | When | Content |
|---|---|---|
| Processing | Any trip of the user still has files from an upload in the queue | Spinning icon, "Processing 1 file…" / "Processing N files…" |
| Passkey offer | Once after a magic-link sign-in | See auth.md |
Statistics¶
The same blocks sit on the logbook and on the public skipper profile. All count over the counted days (app/services/logbook_stats.rb); "standard" blocks are for every user, PRO blocks only for PRO users (User#pro?; until a plan model exists every user is PRO). Standard users don't see the PRO blocks at all.
Totals¶
Four readouts in a row (two by two on narrow screens), each with a small line below. Standard.
| Readout | Value | Small line | Rule |
|---|---|---|---|
| Distance | X.X NM; (?) "Includes X.X NM bridged" when bridges exist |
"Sailed X.X · Engine X.X" | Sum of the counted days plus tracks without a trip; sailed / engine from the days' and tracks' split |
| Skipper miles | X.X NM |
"N % of miles · N % of trips" | Distance of counted days on trips the user skippered (own with "I was the skipper", or the crew's skipper), whoever recorded them. Tracks without a trip never count. % of trips: skippered trips ÷ trips with a counted day |
| Days on the water | Integer | "Sail N h · Engine N h" | Distinct dates of the counted days; two trips on the same date count once. Hours: sums of the counted days plus tracks without a trip |
| Night miles | X.X NM; (?) "Miles underway in nautical night: the sun more than 12° below the horizon." |
"N h at night" | Sums of the days' night miles and hours |
The statistics are cached per user and refresh whenever a track or trip of the user, a day of a trip they crewed on, one of their crew memberships, or the boat of a counted trip changes.
Counted days¶
A day's miles never count twice (app/services/logbook_stats.rb). Trips the user crewed on (sharing.md) count too, by day:
- Every day of the user's own trips counts. Two own trips on the same date both count (morning and afternoon are different sailing).
- A day of a trip the user crewed on counts only on a date that has no own trip day and no own track without a trip (that track's date: its kept start in UTC).
- Of several crewed days on such a date, only the one with the longest distance counts.
The trip page always shows the trip's full numbers; only the statistics skip days. The owner's totals don't change when someone joins their crew.
Distance per year¶
Standard. Card "Distance per year", shown only when at least one day exists. One row per year, newest first: the year, a bar (width relative to the longest year) split into sailed (blue) and engine (red; bridged miles count here), X.X NM; tooltip "Sailed X.X · Engine X.X". Legend "Sailed", "Engine" below. Over the counted days: a day counts in the year of its date; a bridge counts on the day it ends.
More (PRO)¶
Heading "More" with a PRO badge, then two cards side by side (stacked below 860 px):
How you spent the nights (on the profile "How the nights were spent"): a ring (pie) chart with the number of nights in the middle and a legend with counts: In port (mooring, purple), At anchor (anchor, green), On a buoy (buoy, orange), Lines to shore (shoreline, ochre), At sea (at_sea, ink). A night is a counted day's night stay (override first); the last day of a trip has none. Nights with stay unknown stay out of the chart: "N nights where we don't know how they were spent" below. No nights: "No nights on board yet."
Boat length and type: one row per length band of the trip's boat, only bands with miles: "under 10 m", "10–12 m", "12–14 m", "14–16 m", "16 m +", "Length not set" (lower edge included). Each bar is split by boat type (sailing yacht blue, catamaran orange, motorboat red), width relative to the longest band, NM on the right. Legend: each type with its miles. Note "NM per length band · Longest skippered: X.X m" (the longest boat on a skippered trip, when known). No trip with a boat: "No trips with a boat yet. Give your trips a boat to see this."
Globe (PRO)¶
Card "Your trips on the globe", only when a trip has a start position. A globe (orthographic projection, Natural Earth 110 m land, globe_controller) with one orange dot per trip at its exact start (the first point of its first day), sized by the trip's counted miles; it opens turned to the centre of the starts.
- Drag (or arrow keys) turns it; mouse wheel, pinch, + / − keys and the zoom buttons zoom 1×–8×.
- A click on a dot opens a popup: title, "Mon YYYY · X.X NM", "Open trip →" (the trip page). Dragging closes it.
- Under the globe: "Drag to turn · scroll, pinch or +/− to zoom · click a dot for the trip". Screen readers get the trips as a list of links.
Latest trips¶
Heading "Latest trips" with a primary button "All trips →" (/my/trips, the same size and colour as "Invite crew" on the trip page). The 3 trips (own or crewed) whose first track started most recently (after trims; trips without tracks last), as trip cards in a grid. When there are more than 3, an orange "More" button (primary, → /my/trips) sits centred below the grid, as wide as the middle of three cards; full width at 640 px and narrower.
Empty state: compass icon, "No sailing trips logged yet", "Record an activity on your Garmin watch or upload a FIT or GPX file to see your trips here.", buttons "Pair your watch" (→ /pair) and "Install guides" (→ /help#install).
Live refresh¶
The page subscribes to the user's logbook stream. When an upload finishes importing, the page refreshes itself (Turbo morph).
Trips list (/my/trips)¶
Page head "Trips" / "Every trip you sailed, newest first." with Upload tracks, then the processing banner (as on the logbook).
Filters¶
A GET form; every change submits it at once (without JavaScript a "Filter" button appears). All values sit in the URL. The list holds the user's own trips and the trips they crewed on (sharing.md).
| Filter | Options | Rule |
|---|---|---|
| Year | All, then every year with a day of an own or crewed trip, newest first | A trip matches if any of its days falls in that year |
| Sailing area | All, then the distinct areas of own and crewed trips A–Z | Exact match |
| Boat | All, the user's boats A–Z, "No boat" | "No boat" = trips without a boat |
| Skipper | All, "As skipper", "Not as skipper" (skipper=yes / no) |
Trips the viewer was skipper on: their own with "I was the skipper" (trip.md), or a crewed trip where they are the crew's skipper (sharing.md § Roles). Any other value counts as All |
| Order | Newest first (default), Oldest first, Longest first, Name A–Z | Newest / oldest by the start of the first track (after trims), trips without tracks last; longest by total distance; name ignores case |
"Reset" (link to /my/trips) appears when a year, area, boat or skipper is chosen. The order alone doesn't count as a filter.
Phones (640 px and narrower): only Order shows, with an orange filter button (primary, icon-only, aria-label "Filters", aria-expanded) to its right. The button folds Year, Sailing area, Boat, Skipper and Reset out below them; they start open when a filter is set (filters_toggle_controller.js). Without JavaScript every filter stays visible and the button is hidden.
List¶
Trip cards in a grid, 24 per page. Pager buttons "Newer trips" and "Older trips" appear when there is a previous or next page.
| Empty case | Content |
|---|---|
| No trips at all | Same empty state as the logbook |
| Filters match nothing | "No trips match these filters." + button "Reset" |
The page refreshes itself when an upload finishes, like the logbook.
Trip card¶
Used on the logbook and the trips list. The whole card opens the trip. Every card has the same rows so they line up.
| Element | Content |
|---|---|
| Mini map | Static map of the trip (no zoom, no drag): the trip's stored card line (at most 600 points, ingestion.md § Trip aggregates), sailing blue, engine red, no markers (no states, nights or events: only the line). Styling as on the trip map. |
| Skipper band | Only when the viewer was skipper (their own trip with "I was the skipper", or the crew's skipper): a diagonal band across the map's top left corner (rotated −45°, 136 px long, its ends cut off by the card's rounded corner), "SKIPPER" in white 11 px bold capitals on dark orange #B94409 (the light --signal-ink; always on the light map, so the white text stays readable) |
| Crew marker | Top right, only on a trip the viewer crewed on: crew icon in a 32 px circle, tooltip "You are on the crew of this trip". Own trips have nothing there. |
| Processing badge | Top left, "Processing N files…" while an upload for this trip is queued; right of the skipper band when there is one |
| Title | Trip title. A dot after it when a track is still syncing or incomplete (tooltip "A track of this trip is not fully synced yet"). |
| Dates | Date range of the trip's days, or "No days yet" |
| Mini stats | X.X "NM" · sailing hours 5.5h "Sail" (blue) · engine hours 1.2h "Engine" (red) |
| Boat row | " |
| Area row | Sailing area or greyed "No sailing area" |
| Charter row | "Own boat", the charter company, or greyed "Charter or own boat not set" |
| Footer | "N days" · "N tracks" |
Upload dialog¶
The Upload tracks button on the logbook and the trips list opens a modal dialog; Add track on a trip page opens the same dialog with "Add to an existing trip" and that trip already chosen (trip.md). It takes FIT and GPX files, and all files of one upload go into one trip. A FIT file holds one track, a GPX file one per <trk> (a whole holiday exported from the Garmin Desktop App comes in as one file with one track per day). How the files are imported is in ingestion.md.
Layout¶
| Area | Content |
|---|---|
| Head | "Upload tracks", close button |
| "Put the tracks into" | Radio A new trip (default) with a "Trip name" field (max 200; placeholder "Trip |
| Bridges | Checkbox "Bridge gaps between tracks" with hint "Connects every track to the end of the one before with a straight line, for stretches that weren't recorded (watch forgotten, battery dead). Those miles count as distance, not as time." Picking an existing trip shows that trip's current setting; changing it here changes the trip. |
| Drop zone | Title "Drop FIT or GPX files here or click to choose them", or "Choose FIT or GPX files" on a touch screen (pointer: coarse); hint "Up to 20 files, 100 MB each. In Garmin Connect, open an activity and choose Export original." The whole area is the label of the file input: a click or tap opens the file picker (Space or Enter on the focused area too); with a mouse, files can also be dragged onto it. The picker doesn't filter by type (Android can't pick .fit or .gpx files behind a type filter); each file is checked as below. |
| File rows | One row per file (below) |
| Actions | "Cancel" · "Add to logbook" (disabled until at least one file is ready and none is still uploading) |
File rows¶
Each picked file is sent on its own (POST /my/trips/upload, step=preview), decoded but not imported, and comes back as a row.
| Element | Content |
|---|---|
| File name | As picked |
| Progress bar | Upload progress up to 90 %, 100 % when decoded |
| Meta | " |
| Seen before | Orange badge "Seen before: skipped" when the user already has a track that started within 60 s of every track in the file |
| Remove | Trash button drops the row |
Rows with a problem show the error instead of the progress bar and don't block the others:
| Problem | Message |
|---|---|
| More than 20 files | "One upload takes up to 20 files." |
Not a .fit or .gpx name |
"That isn't a FIT or GPX file. In Garmin Connect, open an activity and choose Export original; other apps export GPX." |
| Larger than 100 MB | "This file is bigger than 100 MB." |
| Can't be decoded | "This file can't be read. Is it a FIT or GPX file?" |
| No GPS points | "This file has no GPS track." |
| GPX with only a route or waypoints | "This file only has a planned route or waypoints, no recorded track." |
| GPX points without times | "The |
| GPX times in a format SailEver can't read | "The |
| GPX points all at one time | "All |
| Network or server error | "The upload didn't work. Please try again." |
While "A new trip" is chosen and the name wasn't typed, the name fills itself as "Trip
Add to logbook¶
step=commit sends the trip choice, the bridge setting and the ready files.
- A new trip is created with the name (or the default) and the bridge setting.
- The chosen trip always wins over a trip token inside the file.
- Files seen before are skipped and named in the notice, never moved. Of a GPX file only the tracks seen before are skipped; the others go in.
- The dialog closes and the trip opens (
/my/trips/:id, the new or the chosen one) with a notice: "1 file is being added to your logbook." / "N files are being added to your logbook.", plus "was already in your logbook and was skipped." / "N files were already in your logbook and were skipped: a, b." If nothing is left: "No new tracks to add."; when that leaves no trip (every file seen before, "A new trip" chosen), the dialog closes and the current page reloads with that notice instead. - The trip shows "Processing N files…" until the background import is done; then the page refreshes again.
- If the commit fails, the button is enabled again and the browser shows "The upload didn't work. Please try again."
Closing the dialog drops all rows. Decoded files that were never committed are deleted after a day.
A new trip from an upload gets the new-trip email (emails.md).
Boats (/my/boats)¶
Page head "My Boats" / "Manage your sailboats and motorboats." with the orange Add Boat button (plus icon, → /my/boats/new), then the boat list.
Boat list¶
Table sorted by name: Name (bold), Model, Type, Length ("12.8 m" or "–"), Home Port, Charter company ("Own boat", the company, or "–"), and an "Edit" button.
Empty: boat icon, "No boats added yet", "Add your boat with “Add Boat” to assign it to sailing trips."
Add New Boat (/my/boats/new)¶
Back link "Boats", title "Add New Boat", the fields below, "Cancel" and "Save Boat".
| Field | Rules |
|---|---|
| Boat Name | Required, placeholder "Luna" |
| Model / Type | Required (server check), placeholder "Hanse 458" |
| Type | Three choices with icons: Sailing yacht (default), Catamaran, Motorboat |
| Length overall (m) | Optional, 2–100 m in 0.1 m steps; hint "Optional. Used for your statistics (miles by boat length)." |
| Home Port | Optional, placeholder "Kiel, Germany" |
| Charter company | Optional, placeholder "Navigare Yachting" |
| Own boat | Checkbox. When ticked the charter company is cleared; hint "Your own boat, no charter." |
Button "Save Boat" → notice "Boat created successfully!" Errors show above the fields (e.g. "Boat model can't be blank").
Edit boat (/my/boats/:id/edit)¶
Back link "Boats", title "Edit boat", the same fields, "Cancel" and "Save Boat" → "Boat updated successfully!"
Danger zone "Delete boat": "Removes the boat. Trips sailed on it keep their tracks but no longer show the boat." Button "Delete boat", confirm "Are you sure you want to delete this boat?", notice "Boat deleted successfully."
A boat can also be created from the trip edit page (trip.md).
Known deviations¶
- Trip cards show hours as decimals (
5.5h, labels "Sail"/"Engine"), unlike the5 h 30 minformat used everywhere else. - The empty state says "Record an activity on your Garmin watch", which leaves out Wear OS and uses "activity" for a track.
- Days of tracks that are not in any trip don't count in "Days on the water" or the year chart, while their distance and hours count in the totals.