The admin screen's model list (/api/tags) and pull (/api/pull) went out
without the Authorization header that the extraction and /api/show already
send, so an Ollama behind a Bearer proxy answered 401 and the list came back
as a 502. The controller now adds the key saved with the local provider, read
server side through LlmConfigResolver.readLocalServerAccess, and only when the
requested base URL reaches the same server root as the saved one, so another
address in the form never receives it.
A 401 or 403 now says whether the key was rejected or none is saved for that
address, and the admin form shows the server's reason instead of the axios
status line. A server without /api/tags (LM Studio, vLLM) is asked for its
OpenAI-compatible /v1/models instead, and the list body is read with a size
cap and checked before use.
Refs #2649
The TREK Places service takes an optional lang now and puts localities into
it, and names its OpenStreetMap layer rows in it with the settlement they are
in. TREK never sent one, so a Japan trip in an English UI kept reading
"福岡市博多区" and the layer rows had no place at all, only the local name.
Search, autocomplete, nearby and the details of an index place now ask in the
reader's language (TREK codes through toApiLang, anything that is no language
tag left off). A layer row with a locality reads "Sumida, Japan" like an index
row, the country named from its coordinate; without one it keeps the local
name as before. The offline area download stays without a language. REST and
MCP share the service methods, so both get it.
Refs #2592
Deleting a booking also deletes the expenses linked to it, but the
question only said the booking would be deleted. A booking with a linked
expense now says so in every place that asks: the bookings list and detail,
the transport and transit dialogs, and the phone's booking cards. The MCP
delete_reservation description names the linked expenses and the stay a
hotel booking created as well.
Refs #2604
The three planner view switches lived in sessionStorage only, so a new tab,
another browser or another device opened every trip on the plain view again.
They are now kept per trip in one user setting, planner_trip_views, validated
against a contract in @trek/shared. The server keeps only switches that are on
and only trips the user can still open, so a deleted trip or one the user left
drops out on the next write. The tab copy in sessionStorage stays and answers
until the settings have loaded.
Refs #2639
The userinfo endpoint answered email_verified: true for every account, although
nobody had confirmed the address. It now follows users.email_self_asserted, the
same mark the SSO login uses before it links an account by email.
An SSO login that matched no account by its subject took the account with the
same email address and linked the provider identity to it. That address proves
nothing when the account typed it in itself: anyone could sign up with, or
switch their profile to, a colleague's address and receive the colleague's
first SSO login. The provider switch branch (#2110) relinked a row on any email
match without even asking for email_verified, and also for another subject of
the same provider.
users.email_self_asserted remembers an address the account set itself: signup
(not the first account) and an email change in the profile settings set it, an
admin entering a new address clears it, and accounts the provider creates start
without it. Existing accounts keep linking, except those whose address moved
away from the registered one without an admin editing it. A login by email now
needs a verified address and an unmarked account, and is refused for another
subject of the same issuer, with email_link_blocked and a translated message on
the login screen. An email change in the profile settings writes a
user.email_change audit row.
Refs #2110
A guest's payer rows and recorded payments hung on the users row by ON DELETE
CASCADE, so deleting the guest wiped money that had really moved: expenses
they paid were left without a payer, settled payments reopened, and a custom
or receipt split lost the guest's share so the balances no longer summed to
zero.
Deleting a guest who paid for an expense or is part of a recorded payment is
now refused with a 409 (REST and the delete_trip_guest MCP tool, same text),
and the share dialog says to change the payer or delete the payment first.
Otherwise the guest's share is handed on: an equal split re-splits as before,
a custom amount goes to the others by largest remainder in the currency's
smallest step (members without an amount get one, so the expense stays a
custom split), and a receipt drops the guest from its lines and is split
again like the cost editor does. A legacy paid_by_user_id is cleared as on
account deletion. Account deletion goes through the same re-split. The
confirmation text in all languages and the wiki say what really happens.
Refs #2604
The night went to the last visit of the hotel. On a day without hours the
booking seats its own stop first, and Days does not show that stop, so a rest
the traveller put mid-day was the last visit: it was held to the check-in and
the places behind it followed from there, the way the report describes.
The night is now the visit that ends the day, else the booking's stop, else
the last visit. A visit the traveller adds to the check-in day no longer moves
the night or its check-in. nightVisits in shared holds the rule, and the road
trip, calculate_roadtrip, the stored ride slot and the stop editor all read it.
Refs #2636
Creating the expense from a booking opened the editor on today, and the
cost a booking import writes by itself had no date at all, so it sat under
"No date". Both now take the booking's own date, else a hotel's check-in
day, else the day the booking sits on. The server sets it for every
booking write that creates a cost, so REST, MCP, plugin RPC and the import
agree.
A start time typed on a booking with no day picked no longer borrows
today's date on desktop. Today was mostly outside the trip, the server
then moved the booking onto day 1 while the list kept today. Desktop now
does what the phone already did and keeps the time without a date.
Refs #2604
The ledger nets every currency in hundredths, and the settlement handed those
hundredths out as they were. For a currency shown without decimals the panels
then rounded each balance and flow on its own: 10,000 KRW three ways read
+6,667 / -3,333 / -3,333, two flows of 3,333 sat under "You're owed 6,667",
and Settle booked 3,333.34 won.
The balances, flows and final budget lines are now allocated by largest
remainder in whole steps of the display currency (whole won or yen, unchanged
for two and three decimal ones), so the shown set adds up to zero and the flow
amount Settle sends is the whole amount on the row. The ledger, stored rows
and manual settlements are untouched. REST and the MCP settlement summary
share the same method.
Refs #2604
Moving a trip's start shifted the owner's vacay entries one row at a time,
checking each target against rows that were about to move themselves. A
block shifted by less than its own length kept its first days behind and
pushed the rest onto the weekend, even with weekends blocked.
The shift now decides every day before writing: a day whose target is a
blocked weekend or a whole company holiday is dropped, as is one whose
target already holds leave that is not moving; a half company holiday
leaves half a day. The rest move in the direction of travel inside one
transaction. The plan is the one the owner is active in, so a member of a
fused plan gets their days moved too, and a trip edit no longer creates a
vacay plan for an owner who has none. Open vacay views are notified after
the trip update commits.
The pure vacay helpers moved to vacay.helpers.ts and the trip window tests
to their own suite, so both files stay inside their size budgets.
Refs #2604
The copy has nobody on it but the copier, yet it took every payer and
split row along. Its balances then had the copier owing people who cannot
see the trip, and the first save of such an expense dropped them and
shrank its total.
An expense anyone else pays for or shares now comes across as a planning
entry with its total and no payers, split or receipt lines; one that is
the copier's alone is copied whole. Assignment participants and the
legacy paid_by_user_id keep only the copier for the same reason.
Refs #2604
Removing someone from a trip leaves their payer and split rows on the
expenses they were part of. Every save ran the ids the form sends back
through the roster filter, so an unchanged save dropped them, total_price
shrank to what the rest had paid and a custom split stopped adding up.
Ids already on the expense now stay allowed, as payer or as member, and
can still be taken off it on purpose. A new expense and an id that never
was on the expense are filtered as before (#1952). The paid ticks of the
split carry over a form save instead of being reset. REST, the plugin RPC
and the MCP split checks all read the same allowed set from the service.
Refs #2604
On an install without the TREK Places index and without a Google key the
suggestions come from Nominatim, and autocompleteNominatim asked it without
the box the client had sent, so every suggestion was ranked globally. The
search fallback on the same install dropped the bias the same way. Both now
pass it on as a viewbox with bounded=0, which ranks results near the trip
without hiding anything further away.
Refs #2620
The suggestion list under the place search showed the index's raw ISO code
("福岡市博多区, JP") and listed a place twice when the index and its
OpenStreetMap layer both carried it, which the search list after Enter
already merges. Suggestions now name the country through Intl in the
language of the search, and a layer row at the spot of an index row with a
matching name is dropped by the same rule mergeSearchResults uses, now
shared as samePlace.
Refs #2592
The create and update answers, the reservation:updated broadcast and the MCP
reservation tools all hand back the single booking read, and that read never
loaded reservation_day_positions. The day list puts a booking moved on a day by
that slot before its time, and the client replaces the booking with the answer,
so a booking moved on a day jumped back to its time after every edit until the
next reload. The read now attaches day_positions in the shape the trip list
already sends.
Refs #2631
A stop counted as the booked night whenever a stay began at its place on its
day, so a hotel on its check-in day twice (the booking's own stop and a rest
the traveller planned there) was read as two nights. Both were held to the
check-in: the rest waited for the room or was set back to it, and with a check-in
far from its arrival the rest and every stop behind it moved onto the next day's
card.
oneNightPerStay in shared keeps the night and its check-in on the last visit of
the place and turns the earlier ones into ordinary stops, still a hotel. The
browser and calculate_roadtrip both run it, the stored ride slot times only that
row by the check-in, and opening an earlier visit in road trip mode opens the
stop rather than the night.
Refs #2636
A stop with a start time but no end time went out as a VEVENT without
DTEND. Calendar clients fill in their own default length for that, mostly
an hour, so a 20 minute stop showed up as an hour. The event now ends at
start plus the place's duration_minutes when that is set and above zero,
carried over midnight like the stay markers. An explicit end time still
wins, so start and end under an hour stay as they were. The trip download,
both calendar feeds and the MCP export share this builder.
Refs #2641
Marking a region wrote its country with source 'manual', the same as ticking
the country off by hand, and removing the last region then took the country
away whatever its origin, Dawarich confirmations included. A country that
only comes in through a region is now stored as 'region', marking it visited
afterwards turns it into the user's own mark, and the cascade in unmarkRegion
only runs for a country without such a mark. Readers still see 'manual' for
those rows, so the country detail, the MCP resource and the plugin RPC answer
as before. Rows written before this change stay 'manual' and keep their
country.
REST, MCP and plugin RPC share the service. The atlas popup on desktop and
phone now asks the server whether the country is still marked before it
drops it from the map instead of guessing.
Refs #2597
NODE_ENV="production " passed the boot check but matched none of the
exact production comparisons, so the web UI was never served and every
page answered {"error":"Cannot GET /"}, while the banner showed a clean
"production". The boot import now trims these three variables in
process.env before anything else reads them, so Express, the plugin
children and readEnv() agree, and reports each change as a process
warning. Casing stays as it was. A padded "test" is only reported, so an
instance cannot switch to an in-memory database after an update. The
banner prints NODE_ENV as set instead of lowercasing it.
Refs #2565
A Google place URL carries two points: @lat,lng is the centre of the map
view, the !3d!4d data param is the place itself. extractCoords took the
camera first, so a pasted share link put the place around 170 m off and
reverse geocoded the wrong street. The data param is now read before @;
the order of @ and ?q=/?ll= is unchanged. REST and the resolve_maps_url
MCP tool share the resolver, so both pick this up.
Refs #2635
Moving a saved place to another list takes it out of the source list, but
updatePlace only asked for edit rights there, so an editor could empty a
shared list they may not delete from. The source now takes owner or admin,
like a delete, and the target still takes edit rights. REST and the
update_collection_place tool share the service path. The Move to list
action on desktop and phone follows the delete permission.
DAY-SVC-048 and DAY-DEL-011 pinned the old reorder, which handed a day
without a date the first slot's date; they now expect it to keep none and
the day it passes to keep its own. OIDC-SSRF-003 reads oidc.service.ts as
text and missed the token call once Prettier put a trailing comma after its
hop budget of 0; the pattern takes the comma.
knip found them once the server lanes and the client ratchets met: re-exports
and helpers that moved into place-import, nest-rpc and auth-core but kept an
export nothing reads, the demo-denied helper the central MCP gate replaced,
and two settings keys only the selector uses. @OptionalAuth stays exported
as the documented opt-out and is marked @public. The baselines for knip and
the unused en keys drop to the merged tree.
Reordering handed out the dates by position, so moving a day added without a
date to the front gave it the first date and took the date of the last dated
day. The undated row stayed at the end and the move looked like it did
nothing, by drag and by the arrows alike. The dates now go to the dated days
only, in their new order; the rule lives in shared so the server, the
planner's optimistic list and the MCP tool apply the same one.
A mechanical prettier --write over server/src, server/tests, server/scripts
and shared/src with the workspaces' .prettierrc; nothing but layout and
import order changes, and every file keeps the same set of imports. shared
is formatted throughout and its format baseline is empty. The server
baseline keeps the 35 files that Prettier would grow past their lint:size
limit (they need a split first, which is not a formatting change), and the
generated entity files stay as their generator writes them.
src/db/entities/ is written by scripts/generate-entities.ts, and
check:entities holds every file byte for byte to its output, which puts the
MikroORM import first where Prettier's import sorting puts it last.
Formatting them would fail that gate, and npm run format would do it on any
run. lint:format no longer scans the directory (its 133 entries leave the
format baseline) and a .prettierignore keeps the format script off it.
Following the method chain was not enough: Prettier also wraps a cast read
in parentheses (const n = ( above db.prepare(), puts a for head above its
body and nests the call in an argument list, each leaving it more than one
line below its marker. When the line under a marker holds no call, the
marker now excuses the first call further down in that statement, up to
the line ending it with a semicolon, never past a blank line and at most
eight lines down. Every file counts the same as before on today's tree and
on the formatted one.
Prettier wraps the emoji regex onto the line after its declaration and
spreads two any-cast lines in the plugin tests over several lines, which
would leave the casts outside the eslint-disable-next-line above them. The
regex directive now sits on the line right before the literal, the dev-link
controller takes never instead of any for the collaborators it ignores, and
the supervisor test reaches the private kill/onExit through one cast.
Prettier breaks a long schema into z on one line and .object( on the next,
which the count read as nothing: formatting the tool files would have taken
134 hand-written fields out of the tally. Whitespace before the dot counts
now. Four files already held such a split call, so their entries rise to
what they really hold (atlas 23, doc-sync 1, help 4, transit 16).
Prettier splits a long db.prepare('...').all() into db on one line and
.prepare( on the next, which put the call two lines below its marker and
made every parity oracle in the repository tests count as a raw statement.
The marker now also covers the method-chain lines that continue the
statement right below it, up to the first line that does not start with a
dot.
The sort-imports plugin moved bare imports along with the rest, which on
src/index.ts would have put reflect-metadata after the modules that need it
and the env validation ahead of dotenv. With importOrderSideEffects off, a
side-effect import stays in place and the imports between two of them are
sorted on their own. The three test files that were already formatted gain
the blank line the new grouping writes after reflect-metadata.
bootTestApp replaced JourneyPhotoCaptureService with a factory calling its constructor positionally, only to hand it an offline geocoder, so a dependency the service gained broke the boot of every MCP suite. The container now builds the service itself and the OsmClient's reverse lookup answers that nothing is known, which also keeps every other reverse lookup a booted suite reaches inside the process.
lint:test-mocks missed vi.mock(import('../src/config'), ...), the typed form Vitest takes, and counted mocks quoted in doc comments, which left slack a real mock could take. lint:test-new-service missed a service built under an import alias or as new (Name)(...). Both now strip comments first and recognise those forms; the baselines lose the entries that were only comments.