Commit Graph

2533 Commits

Author SHA1 Message Date
Maurice 803a7cb64a fix(oidc): bind the SSO auth code to the browser that earned it
The callback hands the finished login back as a code in a redirect URL,
and /exchange turned any code into a session. That made the code the
whole credential while it travelled somewhere URLs are readable: browser
history on a shared machine, a referrer, a reverse proxy log. It cuts
both ways. Redeem a code you saw over someone's shoulder and you are
them; or complete a login as yourself, hand the victim's browser your
code, and they are silently signed into your account and write their
next trip into it.

The callback now also sets a one-minute httpOnly cookie holding a random
secret, and /exchange wants the code plus that cookie. Only the browser
that came back from the provider holds both, which is the same shape the
state cookie already gives the callback. The code is burnt on every
attempt, right or wrong, and an unbound code answers exactly like an
unknown one so presenting one confirms nothing.
2026-09-13 22:03:24 +02:00
Maurice 9cb74e22f6 fix(dawarich): a stay's length was sixty times too short
Dawarich's `duration` is minutes. It writes the column as
`duration_minutes = ((ended_at - started_at) / 60).to_i` in
`app/services/visits/create.rb` and serialises it straight through; the merge
service rounds the same way. TREK read it as seconds and divided by sixty
again, so an eighteen-minute stop reached the suggestion panel as "0 min" and
an afternoon somewhere as "2 min".

Found by connecting a real Dawarich 1.14.5 and comparing: a visit spanning
11:00:00 to 11:18:45 reports `duration: 18`.

The span between the two timestamps is what the panel uses now. It agrees with
the arrival and departure shown beside it and it survives whatever unit a
future Dawarich picks; the reported figure only stands in when `ended_at` could
not be parsed, and is read as minutes there too.
2026-09-13 21:38:20 +02:00
Maurice 8eb936f46f fix(test): the places e2e knows the table the delete path reads
Deleting a place cancels the nights booked at it, and that query hits
`day_accommodations`. This file builds its own minimal schema and did not have
the table, so every delete answered 500 and three cases failed. The assignments
e2e was given it for the same reason; this one was missed.

This is the failure that has been keeping Server Coverage red on dev.
2026-09-13 21:25:07 +02:00
Maurice 6e8aa3ab26 fix(dawarich): say why the address could not be reached
The server works out the reason and names it: a rejected certificate, a name
that does not resolve, a timeout, or the SSRF guard refusing a private address
along with the environment variable that would permit it. All of that reaches
the client as `errorDetail`, and the panel dropped it, leaving one sentence that
reads the same whether the host is off or TREK refused on purpose.

A tester could only report that it did not work. The panel shows the reason
under the message now; the toast stays one line, and the vague codes stay vague
so an English sentence from Dawarich does not land on a German install.
2026-09-13 21:19:42 +02:00
Maurice 76f45512d1 feat(admin): the phone's API-keys card, rebuilt like the desktop tab
The desktop tab was reshaped around one idea: a key should be the exception
rather than the thing you are expected to paste. The phone still had the older
shape, where Google wore the "Recommended" chip and the TREK Places API was not
on the screen at all. Two pages, opposite advice, same setting.

So the phone gets the same three comparable blocks in the same order: the TREK
index first, with its own ribbon and a folded panel of what it does and does not
carry, then the two keys under theirs. The Google caveat is said before the
field rather than after it, because somebody about to paste a key should read it
while deciding. The four Google switches moved under the key they spend, folded
away: they are set once and never touched again, and open they were longer than
the rest of the card together. Open-Meteo left the card entirely, as it did on
the desktop, since it takes no key and had the loudest treatment of anything
there.

The provider choice is TREK's own picker now. A native select is drawn by the
operating system and was the one control on the page that did not belong to the
app.

Its description was also describing a TREK that no longer exists: it said one
service answers place search. The index and OpenStreetMap answer every search
and the setting only decides who is asked once they come up empty
(maps.service.ts, keyedProvider). Both that line and the missing-key notice say
so now, in all 23 languages, and the two key hints have short forms for the
narrow screen.
2026-09-13 21:07:55 +02:00
Maurice c2751d9c80 fix(journey): declare the three field flags on updateJourney
The allow-list learned them and the signature did not, so the only callers that
name them are the tests and only `typecheck:tests` saw it. `typecheck` covers
src alone, which is why this was green locally and red in CI.
2026-09-13 20:44:24 +02:00
Maurice f8ce80da32 fix(mobile): the pills and the badge above them on one ground
Both are white at 28 percent, and they did not look it. The pills carried a
backdrop blur of their own, and a second backdrop-filter inside the panel's own
opens a new backdrop root, so the same colour composited darker one row down.
The panel already blurs everything behind it.
2026-09-13 20:41:14 +02:00
Maurice cbc28fad88 fix(journey): a dead translation key, and two cards that spoke differently
`common.errorOccurred` is in no locale. It sat in the reorder failure toast
already and I copied it into three more places, so a failed dismiss, restore or
reorder printed the key at the reader. They all say `common.errorTitle` now.

The suggestion mark on a phone card was the word spelled out, which took a third
of a 136px card and pushed the date off it; it is the same plus the desktop
suggestion card offers, and the card's italic dimmed title carries the rest.
Dismissing a suggestion from the entry sheet has its own full-width row above
the buttons rather than a two-line label wedged between Delete and Cancel.

And two cards that should have been one design: the dashboard's spotlight trip
wrote its facts in capitals on a panel of dark glass, the journey hero wrote
them in sentence case on a gradient with its badge loose in a corner. The
journey card is built the way the trip card is now, and the trip card's three
pills are set like the DAY x OF y badge beside them, a size down and on the
same ground.
2026-09-13 20:32:49 +02:00
Maurice 15aefa449e feat(journey): the marker card, and what testing it turned up
Everything here came out of putting the first commit in front of somebody and
watching them use it.

**The marker card is one card now, and it carries the entry's photos.** Leaflet
showed a one-line label and the GL map a two-line card, so the same marker said
different things depending on a renderer setting nobody connects to journeys.
Both now build from one module: a strip of up to three thumbnails bleeding to
the card's edges, the name, and the place and date as chips. On the GL map the
thumbnails open the entry's photos; the rest of the card still never takes the
pointer, so the map drags from anywhere on it.

**A pin that would not sit still.** Dragging the map left the marker trailing
behind and snapping back at the end. The app's press-feedback rule puts a 180ms
transition on `[role="button"]`, MapLibre and Mapbox give their marker element
exactly that role, and both rewrite its `transform` on every frame of a
movement. Markers are excluded now — every map in TREK, not just this one.

**The phone card, after four rounds of looking at it.** The day is the card's
own edge rather than a bar across its foot, and the ring is inset because the
carousel clips. A card without a photo is opaque: it was a wash of the day's
colour over the map. The country is a real flag rather than two letters —
Windows has no flag glyphs, so the emoji drew "DE" where a flag was meant, and
the image falls back to the emoji when it cannot be fetched. Mood, weather and
the date share the top edge; the flag sits opposite them.

**The day bar answers to a finger.** Press the date and slide sideways to run
through the days. The bar is still a row of buttons underneath, because that is
the only version of it a keyboard can reach, and each one is 20px tall now
rather than 3.

Also here:

- Videos upload to an entry (issue #2341). The gallery has taken them since
  #823 and an entry could not: everything went through the images-only filter
  and came back 400, reported as "1 of 1 photos failed". Same two-part upload,
  same absence of transcoding, and the editor now previews a clip in a `<video>`
  instead of a broken-image glyph.
- The search box takes the gap the toolbar already had instead of clipping its
  own placeholder.
- Entry cards line up with the day header again: the reorder arrows moved out of
  a column of their own into the feed's padding and appear on hover, and the
  day-coloured ring on the active card is a layer over it — an outline tight
  enough to sit on the card's edge was painting behind it.
- Map attribution is collapsed to its ⓘ. It cannot go away — the OSM licence
  asks for it — but it does not have to lie under the carousel.
- The suggestion dismiss and the day-header plus use TREK's own tooltip rather
  than the browser's.
2026-09-13 19:52:14 +02:00
Maurice c2707b7eff feat(journey): the enhancements from discussion #2299
roel-de-vries pitched seventeen changes to Journey. Four were already built or
rest on a misunderstanding, one cannot work as described, and three would take
away more than they give; the rest are here, plus two things the list did not
ask for.

**The entry you write on the move now has a name.** Quick capture hid the title
field outright and offered a note instead, so every entry caught on a phone
arrived nameless and a day's timeline read as a column of identical
placeholders. One line of input; it is the single change in this branch that
users will feel most.

**The map stops yanking.** Stepping through the timeline called flyTo with a
zoom floor of 12, so reading a journey from a country view snapped to street
level on the first scroll and stayed there — every marker filling the screen,
and no sense of where anything was. Both renderers pan now and leave the frame
the reader chose. The initial fitBounds still decides how close a journey opens.

**The phone timeline is photos.** A 64px thumbnail beside three lines of text
spent the card on a story nobody reads at that size and truncated the title
after a few characters. The picture is the card now, with the name and place on
it, the country's flag and the date in the corners, and the day carried as a
colour rather than as a number nobody was reading. Above the carousel, a
segment per day: it says how long the journey is, how far in you are, and takes
you to day nine without swiping past days three to eight.

Everything else, briefly:

- A plus in each day header, starting a draft on that day instead of on today.
- A journey opens on today when today is part of it, and at the beginning when
  it is not — a journey that ended last spring is something you read.
- Suggestions can be waved away one at a time. The row survives so the trip sync
  does not offer the same place again; journey settings brings them all back.
- Mood, weather and the pros/cons list can be switched off per journey. Nothing
  written is erased — the form stops asking, the values stay.
- The weather fills itself in at the desk the way it already did on the phone.
  The date picks the source, so writing up last Tuesday gets last Tuesday's.
- Enter opens the next pro or con, below the one you are in.
- The verdict moved behind the story's fold, and the fold itself came in from
  nine lines to four.
- The external picker lost the count it was printing twice and some padding it
  could not afford inside the editor.
- The phone can reorder entries within a day at last; the desktop arrows finally
  have translated labels.
- Marker labels are off where a carousel already names the entry, and on where
  they are the only name a marker has.
- Search over a journey's titles, stories, places and tags, accent-blind.
- An entry knows which country it happened in, resolved once from its
  coordinates against the bundled boundaries.

Not built, and why: the photos-for-markers idea needs clustering first or thirty
markers become soup, and the active entry's photo is already on screen in the
card below. Removing mood, weather and the verdict outright would take them from
everyone who uses them, so they became switches. `capture` on the file picker
takes the photo library away on a phone, which is why the camera is a second
input; it is hidden on desktop now instead. The suggestion strip in the new-entry
flow would be a third surface over the same data — tapping a suggestion already
opens the editor filled in.

The one gap the list missed and this branch does not close: Journey is the only
domain with no offline path. That belongs in its own change.
2026-09-13 18:45:57 +02:00
Maurice 6edb4462f9 style(collections): the label chip rounded like the rating beside it
Two badges in one row read as one set only if they share a shape. The rating
was a pill, the label a rounded rectangle.
2026-09-13 17:06:35 +02:00
Maurice 27ce4df6f5 style(collections): the place panel as things on their own ground
The labels tried the cover photo and lost: a blue chip on a blue sky is simply not
there, whatever shadow it carries. They sit beside the rating now, both as badges
on one row — two answers about the place, side by side, each legible.

The note takes the full width on a ground of its own. Loose in the column it read
as another paragraph of the panel rather than as something somebody wrote about
this place.
2026-09-13 17:02:17 +02:00
Maurice 1a95157b3a feat(collections): say what a place is without having to click it
Asked for in the feedback: a wall of round photos, and no way to tell which is
which. "Name and displaying any custom rating would be very beneficial to quickly
seeing what is around you that is of interest."

The collections map had hover switched off. The trip map has told you what a
marker is all along, so it is the same card, now shared instead of written a
third time — it already existed twice, once per renderer, and the two had drifted
in how they resolved the category icon.

The card gains a rating, and so does the marker itself: an amber disc on the
corner where a trip's day numbers sit. The two never collide, because a numbered
stop is one already planned into a day and this answers the question asked before
that. Nothing is drawn for an unrated place, which is most of them.

Also from this round: the place panel's labels move up beside the name, where a
label belongs — read three rows down they looked like a property of the rating
above them — and the rating gets a ground of its own so a line of stars stops
reading as another row of text.
2026-09-13 16:49:20 +02:00
Maurice 8fc788fb37 style(collections): the place footer is one row of things you do
Getting directions sat up by the address, in a pill of its own, while editing and
copying sat in the footer as square buttons. All three are things you do WITH the
place, so they belong together and should look like one set. Navigation moves
down beside them, and the footer takes the shape the navigation button already
had.
2026-09-13 16:35:31 +02:00
Maurice de3b2a6ac7 fix(roadtrip): a check-in sets the hour when nothing before it does
Book a night to check in at ten, pin a stop to three in the afternoon behind it,
and the rail said 13:46. True enough as arithmetic — with no hour of its own the
chain works the first stop backwards out of the first one that has one, so three
minus a fourteen minute drive minus an hour standing there — and nothing anybody
asked for.

A door that opens at ten, with nothing ahead of it deciding otherwise, IS the
hour: the day starts there and runs forwards. Reached later it stays what it was,
a floor, and a floor below the arrival changes nothing — which is what keeps a
hotel behind an afternoon stop at its real arrival instead of dragging it back to
its check-in.
2026-09-13 16:22:07 +02:00
Maurice fcee6bd2c5 fix(roadtrip): sort a booked night by its check-in, and clear the lint the cut left
Pinning an afternoon stop sorted that one and left the hotel it was booked around
sitting behind it. The day's auto-sort reads each stop's own hour, and a booked
night has none: nobody types a time into a hotel row, they type a check-in, and
that lives on the booking. It counts now, so the night moves with everything else
instead of staying where it was dropped.

The assignments e2e builds its own tables and had neither the accommodation
column nor day_accommodations, so every create in that file 500'd as soon as the
insert learned to carry a booking id. Both are there now.

And the shared lint, which is stricter than the other two and which I had not run:
three leftovers from taking the check-out out of the chain, none of them used any
more.
2026-09-13 16:19:57 +02:00
Maurice c351a0471c fix(accommodations): a check-in decides where in the day the night sits
The stop went to the end of the day, always, on the reasoning that a night ends
the day. It usually does — but a check-in at eleven says the traveller is at the
desk before whatever they pinned to the afternoon, and putting the stop behind it
built a drive that reaches the hotel at seventeen past five after a sixteen
hundred it was booked around. The order looked like the traveller's and was not;
it was this code's default.

A booking with a check-in now takes the place its hour asks for: ahead of the
first stop pinned later, and still last when nothing is. Editing the check-in
re-seats it, in both directions, read off the chain rather than recomputed —
the index a fresh insert would get is not the index a row already holds.

Stops without an hour of their own are passed over in either direction. Their
place in the chain came from the traveller, not from a clock, and a night has no
business pushing past one.
2026-09-13 16:06:11 +02:00
Maurice 3f6d1e8ed7 fix(accommodations): re-export the module, not the provider inside it
The container refused to boot: Nest will not export a provider that belongs to an
imported module, and the service moved into the domain module in the commit
before. Re-exporting that module gives every importer the same service it always
got, and the wiring test now asserts the shape that actually boots rather than
the one that only typechecks.

Neither tsc nor the unit tests could see this — the module metadata is only read
when a container is built. The e2e harness does build one, and that is what
catches it.
2026-09-13 15:50:58 +02:00
Maurice 46aa0c6b0a fix(map): the base layer switch stays in its corner when a place opens
Opening a place sent it from the bottom left of the map to the top, which reads
as the control moving house rather than as room being made. Nothing was in its
way: the inspector is a card centred between the sidebars and at most 800 wide,
so the corner it was clearing is one the card never reaches.

Both renderers, since the switch is not supposed to move when an instance changes
its map.
2026-09-13 15:41:46 +02:00
Maurice 77a16d6f26 fix(places): deleting a place cancels the nights booked at it
A stay whose place was deleted kept its row with place_id NULL: still drawn in the
day header, still naming a hotel through its partner booking, and pointing
nowhere. Nothing could reach it any more and nothing cleaned it up.

It goes with the place now, through the accommodations domain rather than a
DELETE in the places path, because a stay takes its partner booking, that
booking's expense and the day stop it wrote with it — none of which is the places
domain's business to know. Both delete paths, single and bulk.

The service moves into an AccommodationsDomainModule, the assignments-domain
precedent: the service never touches PlacesService, only the MCP tool that writes
a place and a stay together does, so it can sit in a module PlacesModule is free
to import without closing a loop. AccommodationsModule keeps exporting it, so
every existing consumer is unchanged.
2026-09-13 15:36:02 +02:00
Maurice 7b50692372 fix(roadtrip): a check-in is a door opening, not an appointment
It is the hour a room becomes available. Reaching it later is reaching it; only a
time somebody pinned to a stop can be missed. Read as an anchor it did the exact
opposite of what it means: arrive at 16:14 at a hotel that checks in from 11:00
and the rail reported five hours fourteen late, then read that 11:00 as earlier
than the 15:00 before it, took it for a new day and pushed the hotel onto
tomorrow. The day it came from was left with one stop and dropped out of the list
altogether, so the stop before it appeared to vanish.

The schedule now takes it as a floor: waited for when the drive gets there first,
and otherwise silent. Waiting still prints in ink, because the hour is one
somebody named; arriving late to it prints as computed, because nothing was
decided.

Same shape of mistake as the check-out, from the other side. One says the earliest
you may be somewhere, the other the latest you may still be — neither says when
you drive.
2026-09-13 15:28:48 +02:00
Maurice 62d4cdcc18 fix(accommodations): catch the nights the upgrade itself could not reach
A migration runs while the old container is still answering. A booking written in
those seconds comes from code that knows nothing about the day stop and lands
behind the sweep that would have given it one: on the test instance exactly one
of ten, and invisible to the drive ever after, with no way back short of saving
it again. The sweep runs a second time, which is safe because the same NOT EXISTS
decides it both times.

The other way into that state is closed too. "Remove from day" in the place panel
took the stop and left the booking, which is the very state this whole change
exists to remove — and it was the only control that could produce it after the
upgrade. A stop a booking wrote is no longer offered there, in either direction:
removing it strands the booking, adding one puts the same hotel on the day twice.
The night is removed where it is made, in the day's overnight block or by turning
it back into a pause in road trip mode.
2026-09-13 15:19:33 +02:00
Maurice a89ad06ba5 fix(roadtrip): a night is over in the morning, not two driving days later
With daily travel times set, a stay was spent only against the hours the window
is open. Twenty-four hours then took two of them to run down: a single night
ended on the day after next, and every stop behind it moved with it.

Night time is time. The hours between one window closing and the next opening now
count towards the stay, so a night that starts just after eight is over just after
eight the next morning and the day carries on from there. An eleven-hour visit
from eight is over at seven in the evening rather than saving its last hour for
tomorrow — the traveller is still there after the driving stops, they simply do
not drive on until the window opens again.
2026-09-13 14:42:54 +02:00
Maurice 7197ec1c96 fix(roadtrip): a stay that outlasts the day takes the road with it
Three things the day boundary was swallowing, all from the same stay.

The stop after it stayed on yesterday. It is reached a full day later but at a
later reading on the clock, and the only rule watching for a new day was "the
clock went backwards" — which that never does. The schedule's own count knows,
as long as it is read as a difference: a day whose first arrivals came out
negative is lifted whole, and reading the lifted offsets raw once moved an early
stop to tomorrow while leaving the late one behind. It now crosses over properly,
marked as having set off the day before.

The morning after started at the exact minute the stay ended, as if the road to
the first stop took no time. The carried minute is a departure; the drive from
there is added to it.

And that drive was invisible. It was on the map and in the day's total, but the
rail jumped straight to a stop at 08:11 after a stay that ended at 08:00. It gets
the same band any other drive gets, above the stop it arrives at — except where a
stop crossed over, which already draws that road under its own block.
2026-09-13 14:34:52 +02:00
Maurice 3d201e1ced fix(roadtrip): the check-out leaves the drive, and a long stay carries the clock
A check-out is the LATEST the room has to be handed back, never the earliest
anybody may drive on, so it says nothing the chain can use. It shaped four things
it had no business shaping: the departure the day was built from, the length the
rail printed for a booked night, a day-selector and a clock field in the stop
dialog, and the branch that sent a night's STAY badge to the booking form because
its length was not editable here. All four go. The booking keeps its check-out;
it is a booking detail and lives under Days, where it is entered.

What replaces it is the thing that was always the honest answer: how long you
stand there. That is the traveller's own statement rather than a deadline, and a
stay running past midnight now carries the clock into the morning after — a stop
pinned before it ends keeps its hour and collects the late finding, as it would
from any leg it cannot make. A stay spanning whole days walks the clock forward
over them. An ordinary evening hands on nothing: the night between two days is
not a wait somebody is serving out.

The stay dialog goes to a full day rather than four hours, since it is now the
only place a night is given its hours, and offers eight and twelve as presets.
Every stop answers the same question the same way.

Overlay panels lose the skeleton placeholder. It was drawn in the page flow, so
the first time each dialog was ever opened a pale block flashed under the planner
and never again once its chunk was cached.
2026-09-13 14:23:49 +02:00
Maurice f4809a67d3 Revert "fix(roadtrip): a night booked into the next morning holds that morning"
This reverts commit d4677bdea, and keeps the departure chip's icon.

The premise was wrong. A check-out is the LATEST moment the room has to be
handed back, not the earliest one you may leave: whoever wants to set off at
seven checks out before that or leaves the bags at the desk. Reading it as a
floor turned a perfectly good plan into a finding, and no amount of wording
around that would have made it true.

The chip keeps the door-out mark. That part says what it always said, only in a
symbol that is not a second clock face beside the arrival.
2026-09-13 14:01:18 +02:00
Maurice d4677bdeab fix(roadtrip): a night booked into the next morning holds that morning
Every day was scheduled from nothing, so a booking that runs into the next day
ended at midnight as far as the chain was concerned. Check in at 08:00 and out
at 08:00 the next day, and the rail was happy to put you somewhere else at 07:00
that morning, with no sign that the room was still yours.

The day a check-out falls in now starts at the check-out rather than at nothing.
A floor, never a move: a stop with a clock of its own keeps it and collects the
late finding any leg it cannot make in time would give it, so a plan that says
07:00 still says 07:00 and merely admits it does not add up. A stop with no clock
starts at the check-out instead of having no time at all.

Only that one day. A stay running Monday to Thursday leaves Tuesday and Wednesday
alone, because you sleep there, you do not sit there.

The departure chip takes the door-out mark instead of a clock face. Beside an
arrival time, a second clock read as a second arrival.
2026-09-13 13:23:32 +02:00
Maurice 6845db4cb8 fix(roadtrip): a check-in is a time somebody chose, so print it as one
The rail prints an hour in ink when a person picked it and in grey when the chain
worked it out. Every scheduler anchors on `time ?? checkInTime`, so a booked night
starts its day on the check-in exactly the way a pinned stop does — but two of the
three then asked only about `time` when labelling the result, and printed that
chosen hour grey.

The answer lived in three places and two of them disagreed with the anchor they
had just used, so it is one function now. Nothing but the rail reads the flag.

Only visible since a booked night became a stop of its own; before that no stop
carried a check-in.
2026-09-13 13:10:58 +02:00
Maurice 274dbea069 fix(planner): hide the booked night where the day list is actually drawn
The filter sat on the planner hook's assignments, but the day sidebar is handed
storedAssignments straight from the store, so it never saw it. The hotel was
listed as an ordinary place right under the overnight block naming it, on every
day someone booked a night for.

Filtered in getDayAssignments now, next to the service-stop filter that is there
for the neighbouring reason. That is the list itself, so the reorder and undo
paths keep reading the full day and nothing shifts underneath them. The phone
was never affected: its timeline reads the hook's copy.
2026-09-13 13:04:19 +02:00
Maurice d0d3f455a8 fix(planner): let the day list see which stop a booking wrote
The row shapers name every column they pass on, and accommodation_id was in
none of them. So the field the day list hides on never reached the client, and
the booked night was listed as an ordinary place next to the overnight block
that already named it. The stop was correct in the table the whole time; the
three read paths just dropped it on the way out.

The stamp on the freshly written stop had the same hole from the other side: the
INSERT went in without the booking id and an UPDATE added it afterwards, so the
row handed back in the answer, the one the client that booked the night puts
straight into its store, carried none. It goes in with the INSERT now.

The shared link gains the same filter. It has no road trip view to need the stop
and already shows the booking as its own chip on the day, so the row was only
ever the same hotel twice.
2026-09-13 12:59:19 +02:00
Maurice e3471e2dc8 fix(reservations): a night booked on the form belongs on the drive too
The booking form wrote the stay row itself, so it was the one door that left the
day stop out. A hotel entered there stayed invisible to road trip mode, which is
the same complaint as before with a different way in: enter the place twice or it
is not on the route.

It writes the stop now, through AccommodationsService rather than a second copy of
its SQL. The permission argument for not doing so does not hold: the night already
shows in the day header the moment the booking is saved, and the stop is that same
night in the other view, not a second edit to the plan.

Two holes go with it. Moving the booking left the stop on a day nobody sleeps there
any more, and deleting it left the stop behind entirely — in both cases still
carrying the booking's id, which is what the day list hides on, so nobody could
reach it to remove it. The stop now follows the stay and goes with it, and only
once the trip_id guard has confirmed the stay was this trip's to delete.
2026-09-13 12:38:21 +02:00
Maurice eff4605ef3 test(accommodations): cover the paths the day stop added, and the ratchet fell on
Six lines and four functions the mirror brought in were never entered by a test,
which is what took the domain under its coverage floor.

The keepStop branch of a delete, the plugin surface's own update, and the
transaction that has to roll a place and its stay back together. Plus every
sender a surface hands announceMirror: REST and the three plugin methods build
one each, and a fixture that swallowed the call let all four go unexercised --
the one link between a stay write and the sockets everyone else is watching.
2026-09-13 12:12:38 +02:00
Maurice edf0538260 fix(roadtrip): keep a booked night out of the day list, and off the traveller's stops
Three things the first pass got wrong.

A stop a booking puts on the day is for road trip mode, which needs the
hotel in the driving chain. The day planner already shows that booking as
its own overnight block, so the row there would be the same hotel twice.
It is hidden in Days whatever the service-stop switch says, and the switch
goes back to meaning what it says: the petrol stations the traveller added.

Cancelling a night from the road trip popup took the stop with it. That
control means "this is a pause now, not a night", and the stop sits
mid-drive where re-adding it would land it at the end of the day. The
booking now hands the stop over instead of deleting it.

Moving a booking whose stop belongs to the traveller put a second one on
the new day and left the first behind, which is the duplicate this change
exists to remove. Only a booking's own stop moves now. Stays booked before
this shipped get theirs from the migration, not from the next edit they
happen to receive.
2026-09-13 01:42:52 +02:00
Maurice 462a208fdf fix(planner): drop the hotel bookend leg when the hotel is the edge stop
The day's accommodation is drawn as a leg to the first and from the last
waypoint. Now that a booked night puts its hotel on the check-in day as a stop
of its own, that last waypoint IS the hotel on most days, and the leg drawn to
it is a round trip of zero kilometres: in the map line, in the sidebar's leg
list and in every exported directions link, which carried the hotel twice in a
row. The defect already existed for a night booked in the road trip; the change
above would have made it the normal case.

Both bookend rules now decline a leg whose edge stop sits at the hotel's own
coordinates, which covers the map, the sidebar, the phone route and both export
links from one place. Compared by coordinates rather than ids, because the
bookend comes off the stay row and the waypoint off the assignment.
2026-09-13 01:13:41 +02:00
Maurice 5e0245046d feat(planner): show the stop a booked night writes without a reload
The server announces the day stop over the socket like any other assignment,
but the socket deliberately skips the session that sent the request, so the one
person who could not see their new stop was the one who had just booked the
night. That looks exactly like the bug it fixes.

The stay writes hand their answer to one applier, which folds the stop into the
store through the same path the socket uses, so the IndexedDB write-through
happens either way and the dedupe by assignment id comes for free. Three
surfaces call it: the day-detail picker, its edit branch and the phone sheet.
The road-trip flow does not, because it writes its own assignment before the
stay and there is never a mirrored stop to apply.
2026-09-13 01:13:33 +02:00
Maurice f0e1da2d81 feat(accommodations): put a booked night on its check-in day
A road-trip stop is a day assignment: the rail, the server-side plan and the
map all build their stops from day_assignments and only look the stay up
afterwards, to hang check-in and check-out on one. A stay written without an
assignment was therefore invisible to every routing surface, so a hotel entered
in the day planner never reached the road-trip view and had to be entered a
second time as an ordinary place. The road-trip side has always written both
halves in one go; this is the day planner catching up, in the domain that
writes the stay.

Booking a night now also assigns its place to the check-in day, inside the
transaction that already writes the stay and its partner hotel reservation, and
types the place as a hotel stop when it carries no type of its own, so it reads
exactly like a night booked in the road trip. Only the check-in day gets a
stop; the later nights ride on the stay row. A place already planned for that
day gets no second stop and the booking claims none, so cancelling it cannot
delete a stop somebody placed by hand. Moving the booking moves its own stop,
deleting it takes its own stop back, and a trip re-date carries it along with
the stay. All four write surfaces (REST, plugin RPC and both MCP tools) run the
same write and announce the same events; the answers carry the stop too,
because the broadcast skips the socket that sent the request.

New nullable day_assignments.accommodation_id marks a stop as a booking's own.
It stays NULL for every existing row on purpose: everything planned before this
was planned by hand, and no backfill rewrites a finished trip. A stay booked
earlier picks up its stop the next time somebody saves it.

The booking form keeps writing stays on its own: that surface is gated on
reservation_edit and the day stop is day_edit work.
2026-09-13 01:13:25 +02:00
Maurice 3ab5ddaacc Merge pull request #2349 from liketrek/feat/dawarich-integration
feat: Dawarich integration
2026-09-13 00:05:05 +02:00
Maurice 8a28d4dd4a test(server): stop racing supertest's own listener in the scope e2e
Three cases fired their reads through `Promise.all`. The Nest app under
test is never told to listen, so supertest binds an ephemeral port on the
first request; four of those at once race four `listen(0)` calls on the
same server and the losers come back as ECONNRESET. It held here and
failed on a loaded runner. The reads run one after the other now, which
is all they ever needed.
2026-09-12 23:55:22 +02:00
Maurice 105bba2b95 docs(wiki): describe the Dawarich integration
What it reads, what it never writes, what happens to the recorded
positions, and how the connection, the suggestions, the Atlas marks and
the wish list behave from the reader's side.
2026-09-12 23:37:13 +02:00
Maurice 5b4a7e82ff feat(client): show Dawarich stays, routes and marks where they belong
The trip planner and the journal get a "From Dawarich" list: the stays
recorded for those dates, each one a row that can be dismissed or turned
into a place or an entry after a review step that shows time, name, note
and day. A place that came from a recording carries the Dawarich mark
next to its name so its origin stays visible afterwards.

The recorded route lies over the trip and journal maps as an overlay,
coloured per day and cut along the reader's own timezone rather than
UTC. It is fetched for the window on screen and never stored.

The Atlas gets its own panel beside the map for the countries and cities
the recordings prove, and the wish list is ticked off there, on the day
of the visit, undoably. The phone has its own take on all of it, in the
mobile shell's colours and with native sheets.
2026-09-12 23:37:05 +02:00
Maurice 5678eed55c fix(client): use TREK's own controls for tooltips and journey dates
Three small things noticed while working nearby, none of them Dawarich:

The map's layer switcher showed the browser's own tooltip while every
control beside it showed TREK's, so hovering along the row produced two
different boxes in two different styles.

The journey entry editor mixed TREK's date picker with a native time
input, and the date, which is the longer of the two, had the narrower
column: "12. Sept. 2026" wrapped while the clock sat half empty. The row
is split in the date's favour now and the picker is disabled together
with the rest of the form instead of staying live behind it.

On the phone's journey, the dock's "+" now turns into an upload button
while the Gallery tab is open, which is what that tab's one big action
actually is.
2026-09-12 23:36:49 +02:00
Maurice 218d8baf0a feat(server): read visits and recorded routes from Dawarich
A new `integrations/dawarich` domain behind its own addon. Each reader
connects their own instance; the API key is encrypted at rest and never
returned. Nothing is written back to Dawarich, and no raw position is
kept in TREK: the recorded route is fetched per request for the window
being looked at and thrown away again.

What it produces are suggestions, never facts. A recorded stay becomes a
proposed place or journal entry, a country or city becomes a proposed
Atlas mark, and a stay near a wish becomes a proposed tick-off; each one
waits for the reader to confirm it. Identity is (user, Dawarich visit),
so a second sync neither duplicates nor overwrites what was accepted,
and deleting the result puts the suggestion back on the list.

The sync runs as a job on the central scheduler, one user at a time,
guarded against overlapping runs.
2026-09-12 23:36:49 +02:00
Maurice dad1316172 feat(api): let public API keys be scoped to single areas
A key could only ever read everything a user owns. When a second service
reads TREK on that user's behalf, that is more than it needs: Dawarich
wants trips, days and places, not notes, bookings or the wish list.

Keys can now be minted with an explicit list of areas, checked in the
guard and shown when the key is listed. Keys that existed before this
keep full read access, so nothing that works today stops working. A key
marked as limited whose list cannot be read grants nothing rather than
everything.
2026-09-12 23:36:09 +02:00
Maurice 906eef4349 feat(shared): add Dawarich contracts
Zod schemas for the Dawarich integration: connection settings, the
suggestion list and its accept payloads, the recorded-track window and
the Atlas country/city proposals. Places gain an optional `source` so a
place that came out of a recording can say so.
2026-09-12 23:36:08 +02:00
Maurice b010305c0d Merge pull request #2348 from liketrek/fix/collab-links-polish
feat(collab): make the links panel look like the rest of the collab tabs
2026-09-12 19:44:20 +02:00
Maurice 3529b72393 feat(collab): give links the header and empty state its siblings have
The tab was built to its own pattern: a bold title where the other four
carry a small uppercase one, a counter instead of the add button, the add
form as a light block sitting on top of the list, and a grey line of text
where every other empty panel shows the mascot. Side by side with Notes it
read as a different app.

It now uses the poll panel's shell: the same header, the same pill button
top right, the same modal for the form, and the mascot holding a chain.

The load effect depended on the toast and translation objects, both of
which are new on every render, so the list refetched constantly and a link
that had just been added was dropped again by the response in flight. It
depends on the trip alone now, and reads the other two through refs.
2026-09-12 19:37:07 +02:00
Maurice dd5b45ffda feat(collab): a chain for the mascot to hold on an empty links panel 2026-09-12 19:37:07 +02:00
Maurice 11b69ae815 test(places): give the hand-built places tables the Amap id column 2026-09-12 18:07:39 +02:00
Maurice 79095823e5 chore(maps): drop the attribution import the tile helper replaced 2026-09-12 18:03:11 +02:00
Maurice f2bd116044 docs(maps): an Amap link needs a coordinate
A bare amap.com/place page carries an id and nothing else, and turning that
id into a location would need a keyed detail lookup the URL resolver has no
user for. It answers 400, which AMAP-084 pins. The wiki offered the link as
paste-able and the parseAmapUrl comment still promised the lookup, so both
now say what the code does.

The provider header also claimed photoRefs/photoBytes were deliberately
left out, naming two methods the places-provider interface no longer has.
The reason is unchanged: an Amap picture comes without a licence statement
we could show, so an Amap place gets its picture from Wikimedia.
2026-09-12 18:03:11 +02:00