Trip page¶
| Rails | |
|---|---|
| Status | ✅ |
| Code | app/controllers/{trips,trip_days}_controller.rb, app/controllers/concerns/trip_page.rb, app/views/trips/{show,show.html+frame,edit,_description,_overview,_view,_map,_boat,_numbers,_days,_tracks,_thead}.html.erb, app/views/tracks/_sync_badge.html.erb, app/views/components/_split_bar.html.erb, app/models/{trip,trip_day,trip_bridge,track_map_line}.rb, app/models/concerns/plain_rich_text.rb, app/helpers/rich_text_helper.rb, app/views/layouts/action_text/contents/_content.html.erb, app/services/map_lines/, app/services/trip_gpx.rb, app/helpers/logbook_helper.rb (map_nights), app/javascript/controllers/{mini_map,day_focus,inline_boat}_controller.js |
Verified against code: 2026-10-25
Purpose¶
One trip (for example a charter week) on one page: where the boat went on a chart, how far under sail and under engine, each day with where the night was spent, and each track with a way into the track editor. The owner and the trip's crew can open it (/my/trips/:id; anyone else gets 404). Crew see it read-only. Sharing (public links, crew links, the crew section and the public page) is in sharing.md.
Terms: trip, track, day, trim, bridge as in glossary.md. How the numbers, days, nights and bridges are computed is in ingestion.md § Trip aggregates; the columns are in data-model.md. Formats follow README.md § Shared formats.
Layout / behaviour¶
Page head¶
| Element | Content |
|---|---|
| Back link | "← Trips" → /my/trips |
| Title | Trip title (<h1>) |
| Badges | "Skipper" (the viewer was skipper: the owner with "I was the skipper", or the crew's skipper, sharing.md § Roles) · "Crew" (crew icon, for a crew member who isn't skipper) · "Processing N files…" while an upload is queued · "Recalculating…" while a track recompute is pending |
| Meta line | For a crew member first "<owner's first name + last initial>'s trip" (crew icon; the masked email when the owner has no name) · Sailing area (pin icon, if set) · "N days · N tracks" · "Recorded with |
| Boat line | Under the meta line, only when the trip has a boat (trips/_boat); each part only when set: boat icon + |
| Actions | Owner and skipper (sharing.md § Roles), 44 px icon buttons with tooltip and aria-label: Share (share icon, opens the link dialog) · Export GPX (download icon) · Edit trip (pencil icon). A crew member has only the pencil, titled Edit description (they may write the description and nothing else). |
The page subscribes to the trip's stream: when an upload import or a track recompute finishes, it refreshes itself (Turbo morph) and the map is redrawn.
Browser cache. The page has an ETag and Cache-Control: max-age=0, private, must-revalidate, plus Vary: Turbo-Frame. The browser keeps it but asks every time. A reload, a visit or a morph refresh of an unchanged page is answered with 304 and not rendered again. The ETag changes with the trip, its boat, each track (including a pending recompute), each day, the bridges, the user, the trip's links, crew and owner, the tab (view) and share, Turbo frame or whole page, the session's CSRF token and the release (config.x.boot_id, new on every boot). Rails adds the template digest and the flash, so a page with a message is always rendered. The page is never cached by a shared cache or a CDN.
Description¶
Under the page head, above the map (trips/_description), only when the trip has one. The text with its formatting (headings, lists, quotes, code, links), then the small grey line "Last edited by description_edited_by_id, description_edited_at, time in the trip's time zone, format :long). Without a name (the editor's account was deleted): "Last edited
Text only, enforced on the server. has_plain_rich_text :description, max: 10_000 (Action Text; PlainRichText): optional, at most 10 000 characters of text, and no attachments or media: a value with an <action-text-attachment>, img, video, audio, source, embed, iframe or object is invalid ("Description can't contain files, images or other media"). An empty editor (<p><br></p>) is stored as blank. Action Text's sanitizer stays on. In the rendered text (layouts/action_text/contents/_content) every link gets rel="nofollow noopener noreferrer" and target="_blank".
Map¶
A locked Leaflet map, full card width: no zoom buttons, no dragging, scroll, pinch, double-click or keyboard zoom (the trip replay is where the trip is explored). Popups on its markers still open. Bottom right the short credit "© OpenStreetMap" and the "i" that opens the map info (maps.md § Credit).
Open the map. An orange 66 px icon button in the map's top-right corner (button_classes(:primary, size: :lg, icon_only: true), 30 px expand icon maximize: four arrows pointing at the corners, aria-label "Open the map" / "Karte öffnen", on every trip page and the public page, not on cards) opens the trip replay in its map view, a page that fills the browser window: /my/trips/:id/replay, on the public page /t/<token>/replay.
Scale. Bottom left, in nautical miles (maps.md § Scale); not on trip cards.
| Layer | Style |
|---|---|
| Base | SailEver's own map tiles (max zoom 18), maps.md |
| Overlay | Depth bands and contour lines (maps.md), OpenSeaMap seamarks (maps.md) |
| Credit | Bottom right, 10 px, no Leaflet prefix (text in maps.md). Required by the OpenStreetMap licence, so it is on every map: the trip map, the mini maps on the trip cards and the track editor. |
| Track lines | Only the kept part of each track (between IN and OUT), from the tracks' stored map lines (ingestion.md § Map lines): simplified to 5 m, at most 10 000 points over the whole trip. Weight 3, opacity 0.9. Sailing and engine in the light --sail / --engine values (vessel-state.md). Motorsailing counts as engine. Each track is its own line, so a gap between tracks is never drawn as sailed. |
| Bridges | Only when the trip's bridge setting is on: straight line from the last kept point of a track to the first kept point of the next, light --rest value, weight 2.5, dashed 6/8 |
| Event markers | 28 px circles (border included), one per track event that has a position (below), except engine on / off and the state changes to sail, engine and motorsail (the line's colour shows those) |
| Note dots | A state change to sail, engine or motorsail that has a note from the track editor: a 12 px dot with a 2 px white border in the state's colour (#0284C7, #E23B3B, #E08A00), like the handles in the editor. Click opens a popup: the note (bold, inserted as text), the place name and the local time. Changes without a note have no dot. |
| Night markers | Where each night between two days was spent (Night markers), drawn above the event markers |
| Live position | While a track of the trip is still being sent by a watch (sync_status receiving): the newest point of that track as a 22 px orange (--signal) dot with a white border and a pulse (none with reduced motion), on top of everything; tooltip and popup "Last position HH:MM" (trip time zone). Under the map: "Recording now. The orange dot is the boat's last position the watch sent, at HH:MM. The page updates itself." Watches send a segment about 15 s after the start and then about hourly (../shared/track-sync.md), so the dot moves at that pace. |
| Fit | Fits all lines, bridges and markers (padding 30, max zoom 15). One point: zoom 13 on it. Nothing: a fixed view at 47.5° N 14.1° W, zoom 8. |
Event markers. Click opens a popup: icon + title, the place name (or "lat, lon" with 4 decimals), the local time HH:MM and, if known, " ·
| Event | Icon | Title | Colours (fill / border) |
|---|---|---|---|
| State change to anchor | ⚓ | Anchor Stop | #fef3c7 / #d97706 |
| State change to mooring | ⛵ | Mooring Buoy | #ede9fe / #7c3aed |
| State change to buoy | 🟡 | Navigational / Mooring Buoy | #fef9c3 / #ca8a04 |
| State change to shoreline | 🏖️ | Shoreline / Dock / Landline | #d1fae5 / #059669 |
| State change to adrift | ⚠️ | Adrift Stop | #ffedd5 / #ea580c |
| Storm warning | ⛈️ | Storm Warning | #fee2e2 / #991b1b |
Engine on / off events get no marker: they sit where the state changes to or from engine, which has its own. Marker titles are English on every locale.
Night markers¶
One marker per night between two days (not after the last day), at the night's position (trip_days.night_latitude / night_longitude), for the night's stay with the override taking precedence (Night column). No marker when the stay is at sea or unknown, or the night has no position.
| Part | Value |
|---|---|
| Marker | 30 px circle (border included, centred on the position), navy #0B1B2B with a 2 px white border and shadow, the stay's night icon (18 px, white, centred) as in the Night column: anchor (At anchor), buoy (On a buoy), harbour (In port), shore (Lines to shore). Tooltip "Night after day N: |
| Popup | "Night after day N" (bold), the night's place name (if any), then the stay label, plus " · N nights" when the next sailing day is more than one night later |
The labels follow the page language (logbook.days.night_title, app.night.*). Place names are inserted as text, never as HTML.
Numbers¶
Three readouts as one unit (one bordered box, three columns, in one row on every screen; on phones up to 520 px with 24 px values and smaller labels), and for the owner, when the trip has tracks, a separate Edit track button to its right:
| Readout | Big value | Small line |
|---|---|---|
| Total | Trip distance X.X NM (tracks between IN and OUT plus bridges) |
" |
| Sailed (blue) | Sailing distance X.X NM |
Sailing time |
| Engine (red) | Engine distance X.X NM |
Engine time |
Edit track button (c-btn--hero): as high as the readouts, filled brand orange --signal with white pencil icon (28 px), "EDIT TRACK" (display font, 26 px) and below it, on the button, "Trim, names, sail and engine" (13.5 px), darker on hover; much bigger than normal buttons. The hint on the button is the one exception to help texts behind a (?). It links to the track editor of the trip's first track (by start time), whose ‹ › go on to the others. On phones it goes full width below the readouts as one line "EDIT TRACK" (22 px, no hint).
Below them the split bar: one bar with sailed (blue), motored (red) and, if any, bridged (grey) shares, and a legend "Sailed X NM", "Motored X NM", "Estimated X NM". With bridges, a (?) next to "Total" holds "Includes X NM estimated where the recording had a gap." Screen readers get "Sailed X NM, motored Y NM".
Tabs: Days | Tracks¶
For the owner, an orange Add track button (upload icon) sits at the right of the tabs. It opens the upload dialog with "Add to an existing trip" chosen and this trip selected; after the upload the trip page refreshes.
A tab bar "Days" / "Tracks" inside a Turbo frame. Switching requests only the frame, and the server answers with the tab alone (show.html+frame.erb), so the map and the numbers are not built again. Days is the default; Tracks sets ?view=tracks in the URL. Track editor saves and deletes come back to the Tracks tab.
Desktop¶
Both tabs are tables.
Mobile web¶
The tables become cards: each cell shows its column name as a label.
Days table¶
One row per day of the trip.
| Column | Content |
|---|---|
| Day | An orange play chip, the day number (bold) and the date Sat 12.9., together one link "Replay day N" to the trip replay on that day , straight into the replay, not the map view (/my/trips/:id/replay?day=N&play=1, on the public page /t/<token>/replay?day=N&play=1). |
| Route | " |
| Distance | X.X NM |
| Sailed | Sailing distance X.X NM |
| Engine | Engine time (duration) |
| Night | See below |
Footer row "Total": sums of Distance, Sailed and Engine.
Rows don't zoom the map (it is locked); the day link opens the replay instead.
Night column. For every day except the last:
| Element | Content |
|---|---|
| Button | Night icon + the night's place name, or the stay label when there is no name. "–" when the stay is unknown. "· N nights" when the next sailing day is more than one night later. |
| Last day | Grey "Last day" |
On phones (720 px and narrower) the column shows only the night icon; the place, the stay label and "· N nights" are left out of sight but still read out by screen readers.
| Stay | Label | Icon |
|---|---|---|
anchor |
At anchor | anchor |
buoy |
On a buoy | buoy |
mooring |
In port | harbour |
shoreline |
Lines to shore | shore |
at_sea |
At sea | wave |
unknown |
Not set | moon |
The shown stay is the user's override if set, else the detected one.
Empty: "No days yet. They appear as soon as a track is recorded."
Night override¶
The night button opens a popover form (PATCH /my/trips/:trip_id/days/:id). Owner and skipper have it; crew and the public page show the night icon and place as plain text.
| Element | Content |
|---|---|
| Legend | "Night after day N" |
| Choices | One radio per stay except "Not set": At anchor, On a buoy, In port, Lines to shore, At sea (with icons) |
| Reset choice | "Use what was detected" with "Detected: |
| Buttons | "Cancel" (closes) · "Save" |
Saving reloads the trip on the Days tab with "Night saved." The override survives every rebuild of the days. An unknown value is stored as no override.
Tracks table¶
One row per track of the trip, by start time.
| Column | Content |
|---|---|
| Date | Sat 12.9. of the kept start, in the trip's time zone |
| Time | HH:MM–HH:MM of the kept start and end |
| Route | " |
| Distance, Sailed, Engine | As in the days table, per track |
| Device | Device name the track was recorded with, or "Garmin" when none is stored. Then the sync badge. |
| Actions | Owner and skipper: "Edit track" (edit icon) → track editor. Owner: "Move" (route icon) opens the move dialog, which also deletes. Skipper: "Delete" (trash icon, confirm "Delete this track? This can't be undone.") on tracks they uploaded. Crew have no actions column. The public page has no Tracks table (sharing.md § Public trip page). |
Move dialog (move_track_controller.js, one <dialog> per row, kept through the page's self-refresh so a recompute finishing doesn't reset it): title "Move or delete track", a single select "Move to trip" with the user's trips (newest first) and "No trip (DELETE)", a (?) "The track and its numbers go to the trip you pick. A trip left without tracks is deleted. \"No trip (DELETE)\" deletes the track.", buttons Cancel and "Move track". Picking "No trip (DELETE)" asks at once "Delete this track? This can't be undone." (cancel goes back to the trip picked before). Saving a trip sends track[trip_id] with from=move to PATCH /my/tracks/:id; only one of the user's own trips is accepted (any other id: 404, ADR 015): both trips are recomputed, an emptied trip is deleted, and the page goes to the new trip's Tracks tab. Saving "No trip (DELETE)" deletes the track (points, events, map line; its trip stays and is recalculated) and goes to the trip's Tracks tab with "Track deleted." This is the one place to delete a track; there is no separate delete button.
Sync badge (tracks from a paired device; complete tracks show nothing):
| State | Badge |
|---|---|
| Receiving | Blue "Syncing from watch" |
| Incomplete | Orange "Part still on the watch", tooltip "Missing parts: #2, #5. Open "Upload tracks" on the watch to send them." |
Sync states are defined in track-sync.md.
Empty: "No tracks in this trip yet."
Crew¶
Below the tabs, the crew card: everyone on the trip, and for the owner the crew link (sharing.md § Crew section).
Edit trip¶
/my/trips/:id/edit. Back link with the trip title, heading "Edit trip". Max width 760 px.
| Field | Content & rules |
|---|---|
| Trip Title | Required, placeholder "Croatia 2026". The fields above and below the description are for owner and skipper; a crew member sees the Description field only (and no danger zone), saves it, and changes nothing else (other parameters are not permitted). |
| Boat | Select of the user's boats A–Z, blank "-- Select Boat --". Only the user's own boats are accepted (any other id: 404, ADR 015). Link "Add new boat" opens a dialog (below). |
| Sailing Area | Free text with suggestions from the user's own areas (datalist), placeholder "Central Dalmatia". On save whitespace is squeezed; if the same area exists in another trip with different case, that spelling is used. |
| Description | Optional rich text (Description), Lexxy editor with its default toolbar (headings, lists, quotes, code, links) but no file or image upload; hint "Optional. Everyone on the trip sees it, and anyone with a public link. The skipper and the crew can edit it." Owner, skipper and crew may edit it (edit_description). |
| Bridge gaps between tracks | Checkbox with the same hint as in the upload dialog. When on, every track is connected to the previous one (ingestion.md § Bridges). |
| I was the skipper | Checkbox, owner only, hint "Shows a Skipper badge on the trip. You'll need this later for mileage confirmations." Setting it ends a crew member's skipper role (sharing.md § Roles). |
| Time zone | Select of all IANA zones, blank "Automatic (from the first position)". Hint "A day starts at midnight in this time zone. It is set from the first position of the trip." |
Buttons "Cancel" and "Save Trip". Saving recomputes what the change affects: "Bridge gaps between tracks" the bridges, totals and days, the time zone the days; title, sailing area, skipper and boat nothing. Then it goes back to the trip with "Trip updated successfully!" Errors show as a list above the fields (e.g. "Title can't be blank").
Add new boat dialog. Same fields as on the boats page (logbook.md), "Cancel" / "Save Boat". It saves without leaving the page and selects the new boat. Errors show in the dialog, joined with commas, or "The boat couldn't be saved."
Danger zone "Delete trip": "Deletes the trip and all its tracks. This can't be undone." Button "Delete trip", confirm "Delete this trip and all its tracks? This can't be undone." Deleting the trip deletes its tracks with their points, events and FIT files (data-model.md). Then /my/trips with "Trip deleted successfully."
GPX export¶
GET /my/trips/:id/export.gpx downloads a GPX 1.1 file. Owner and skipper (crew: 404).
| Part | Content |
|---|---|
| File name | sailever-<trip-title-slug>-<date of the first kept point in the trip's zone>.gpx (slug "trip" if the title has no usable characters) |
<metadata><name> |
Trip title |
One <trk> per track |
By kept start. Name = " |
| Points | Only kept points (between IN and OUT), each with lat, lon and <time> in UTC |
| Not exported | Bridges, events, speeds, vessel states |
States and edge cases¶
- A trip with no tracks shows an empty map at the fixed default view, zero numbers and both empty states.
- The time zone defaults to UTC until it is set from the first position.
- Large trips: the trip map keeps at most 10 000 points; when the tracks' lines together have more, the points furthest off the line stay. The numbers are not affected.
- A track whose map line isn't stored yet (imported before map lines existed, until
map_lines:backfillhas run) shows every n-th kept point, at most 2000.
Known deviations¶
- The marker for
mooringsays "Mooring Buoy", while everywhere elsemooringmeans "In port". Marker titles are not translated. - Event markers use their own colour palette, not the data colours.
- The "Device" column falls back to "Garmin" even for Wear OS tracks.