v4.3.0 - #2273
Open
mauriceboe wants to merge 152 commits into
Open
Conversation
…into a warning (#2226) * feat(plugins): TREK_PLUGINS_IGNORE_TREK_RANGE turns the version gate into a warning A plugin whose author has not updated its `trek` range for the running TREK can now be installed, updated and activated when the operator sets TREK_PLUGINS_IGNORE_TREK_RANGE to a truthy value. Every gate (registry picker, post-extraction re-check, sideload, dev-link, activation) warns instead of refusing; a missing range is tolerated the same way; "install latest" takes the newest published version. The plugin-API version gate is not lifted and the Discover compatibility verdict stays truthful. The admin is warned at every step: each bypass is logged, install/upload/link/ update responses carry a `trekRangeBypassed` marker, and both admin panels show a "Version checks off" pill, an "Install anyway" confirm dialog before a registry install, the same notice after a path that could not ask first, and a persistent chip on the row while the plugin runs outside its range. Nine new i18n keys in every locale; wiki pages updated (Environment-Variables, Admin-Plugins, Plugin-Development, Security-Hardening). * refactor(plugins): one useRangeBypass hook behind both admin shells SonarQube flagged PR 2226 at 10.8% duplicated new code: the range-bypass logic had been added to the desktop and phone plugin panels as identical copies. The marker/warning types, the row chip, the "Install anyway" offer, the dialog copy and the confirm-or-notice state now live in useRangeBypass (next to useInstanceSettings, the existing shared-logic precedent); each panel keeps only its own dialog/sheet markup. * docs(claude): record the SonarQube gate rules and the no-English-placeholder i18n rule The CI SonarCloud gate (Sonar way, on new code: >=80% coverage, <=3% duplication, A ratings, hotspots reviewed) was only discoverable by waiting 20-25 minutes for the workflow. The root CLAUDE.md now states the conditions, the desktop/mobile shell-mirror duplication trap and its shared-hook fix, and how to check a PR with the sonarqube MCP first; client and server CLAUDE.md point at it. shared/CLAUDE.md says every locale needs a real translation — parity cannot tell an English placeholder from a translation. * docs(claude): keep CLAUDE.md to commands, invariants, gates and philosophy Verified every concrete claim in the five CLAUDE.md files against the code and fixed the stale ones: the websocket server is the Nest realtime gateway, MCP tools live in <domain>.mcp.ts files, the client "native shell" section described code that was never written, offline maps also prefetch vector basemaps, plus several renamed files and wrong table names. Then trimmed everything that drifts on its own: dates, migration history, counts, and inventories of stores, slices, domains, sync files and CLI commands. What stays is the commands, the invariants with the file that enforces each one, what CI actually runs, the reference implementations to copy, and the rules for new code. Each file now opens with that rule.
An expense saved with "No one paid yet" was still charging its split members in calculateSettlement while crediting nobody, so the ledger drifted off zero by the unpaid total. The greedy simplifier that turns balances into payments has no memory of which expense produced which debt, so it handed that phantom amount to whoever happened to be a creditor from an unrelated expense. In the report, two people each owed about 80 to someone who never paid that bill. Booking every payment it offered then left one member permanently short, next to an "everyone square" line. Both cost panels already promise the opposite: they label such a row "Unfinished, total only, not settled yet" and count it into the Outstanding amount card instead. The server now agrees with them. The guard deliberately does not also require a non-zero total, unlike the panels' label predicate. PUT /budget/:id/payers with an empty array zeroes total_price without re-applying it, which would otherwise leave a custom split debiting its members against no credit at all. The #1543 fixture and BUDGET-SVC-DB-004/005 were re-baselined: they pinned the old arithmetic through a deliberately payer-less 9 000 RUB item. The wiki said the old behaviour was intended, so it is updated too. Closes #2225
The note website round-trips correctly through the API and the desktop grid renders it, but the mobile notes tab never carried the field: the form had no input, the payload had no key, and neither the card nor the read-only sheet showed a stored link. Since an editor only ever opens the form and a viewer only ever opens the sheet, no role on a phone could see or reach a website saved from a desktop. Editing a note on mobile did not wipe an existing link. The key was omitted rather than nulled, and the server's CASE WHEN guard kept the stored value, so this was a visibility gap rather than a destructive one. The form now always sends the field, null included, because the server only clears on an explicit non-undefined value. Every render path goes through safeExternalHref: the REST contract accepts any string, so an unguarded href would execute a stored javascript: value in this origin. The desktop expanded view had the same omission and is fixed alongside it, so a member without collab_edit no longer loses the link there either. Closes #2222
POST /oauth/register refused every single-label private-use scheme, so an
MCP client registering workbuddy:// could not connect at all, while the
error it got back advertised "a private custom scheme" as supported.
The cause was two validators that had drifted apart, both of which run on
registration. The provider required a dot in the scheme name, which RFC
8252 section 7.1 only recommends and which protected nothing
(javascript.evil:// passed it). The service behind it had no
custom-scheme branch whatsoever and no [::1], which is what actually
answered the reporter's second symptom, and its localhost exemption was
not gated on http:. So javascript://localhost/%0aalert(1), blob:, about:,
ftp: and wss: were all accepted through the settings UI, and the consent
flow assigns the stored URI to window.location.href. Only the CSP stood
between that and a navigation. Both now decide through one policy in
oauth.helpers.ts.
Widening the scheme set means the deny-list has to carry its own weight:
it grows by the browser, extension and OS-handler schemes that were
previously blocked only as a side effect of the dot rule, including
ms-msdt (Follina) and the ms- namespace by prefix. intent: is denied,
because Chrome follows its embedded fallback URL, which turns it into an
open redirect the authorization server cannot inspect.
Fixing registration alone would have moved the reporter's [::1]:{port}
failure one step later: TREK matched redirect URIs byte for byte while
RFC 8252 section 7.3 frees the port for loopback, so a native client on
an OS-assigned port got a 302 to consent and then invalid_redirect_uri.
The matcher now relaxes the port, and only the port, for http loopback.
Stored URIs are never canonicalised, so the %3A round-trips untouched.
Note this tightens POST /api/oauth/clients for inputs it accepts today.
That is the security half of the change and applies at create time only,
so no already-registered client changes behaviour.
Closes #2227
… it says Four separate defects sat behind "offline mode is broken". The app shell. On the first online launch after a version bump, App.tsx deleted EVERY Cache Storage bucket and unregistered EVERY service worker, then reloaded. That is exactly wrong for a Workbox autoUpdate build, which swaps its precache atomically on install and never leaves an empty one. Between the purge and the completion of a fresh 22 MB precache the origin had no shell at all; anyone who closed the app or lost signal inside that window could no longer start the PWA offline. It also threw away the map tiles and documents the user had downloaded on purpose. It now asks the worker to fetch the new build and reloads once it has taken over. The version marker also moves inside the guarded path, because it was written after the purge and outside its try: a throwing setItem left the caches gone, the marker unwritten and the reload unreached, repeating on every launch. The reported version is validated before it reaches this device's storage and before it is compared against the stored marker, so a value that is not a release tag can neither be persisted nor start an update on every launch. The session. loadUser's offline branch kept the persisted session but never called onAuthSuccess, the only caller of setAuthed(true), and the mutation queue's flush, syncAll and prepareForOffline are all gated on it. A session that launched without network could therefore never sync again for the rest of its life, even after the signal returned: the offline settings buttons spun and returned instantly having done nothing, and queued edits never uploaded. It also left reads pointed at the anonymous database instead of the user's own. The date rule. A trip is only cached when it has not ended, and one that ended over a week ago is actively evicted. The per-trip switch was an opt-out list, so ticking a finished trip changed nothing at all. For anyone whose trips are all in the past, "Download for offline use" stored none of them, reported success, and the cache stayed at zero. Switching a trip on now pins it, which overrides both the skip and the eviction, and the switch shows the honest position rather than promising storage that never happens. What the screen says. A sync that never started (already running, offline, signed out) was indistinguishable from one that stored everything: the progress bar painted full and a green "Ready for offline use" appeared over a run of nothing. Both syncs now report why they stopped and the screen says so. A rejected Dexie read no longer renders as "No trips cached yet", and the clear button stays reachable when the database holds data that no bookkeeping row points at. A run that rejects outright says so too: the trip list it starts from is a plain fetch, so a network blip took the whole thing down and left the last progress frame on screen looking like a finished download. The 401-with-HTML branch in the API client now confirms a reachable proxy before tearing the worker down, since several of TREK's own routes answer 401 with a text/html body. Both settings shells move onto one shared hook rather than growing the same logic twice, and the tile prefetch caps its depth when the browser refused persistent storage. Opaque tiles are billed at about 7 MB each, and an origin that is not exempt from eviction loses its whole bucket, precached shell included, which Workbox then never refills because sw.js has not changed. Closes #2228
… honours it The timeout for a model call was spelled three times and adjustable nowhere: 120s in the Anthropic client, 300s in the OpenAI-compatible one, 300s in the Ollama one. None of them was the ceiling that actually applied, either — every one of those calls goes through the pinned undici dispatcher, and an undici Agent stops waiting for response headers after five minutes by default, so a client constant set above that never got the chance to fire. - LLM_TIMEOUT_MS (default 900000) replaces the three constants and is passed to the dispatcher as headersTimeout/bodyTimeout, so the abort signal and undici are governed by the same setting. The default is deliberately generous: heavier parsing work should fit without a code change, and an admin who would rather fail fast can lower it. - The ceiling stays on the model lane. safeFetchAdminConfigured is also the OIDC lane (discovery, token, userinfo, JWKS) and the plugin OAuth token exchange, and that one sends no AbortSignal of its own, so it keeps undici's five minutes rather than inheriting a model setting. - The derived value is floored: the schema accepts any positive number and undici rejects a fractional headersTimeout.
Three shipped sentences became false with LLM_TIMEOUT_MS: two claiming the addon needs no environment variables (one of them in the very file the variable was added to), and one naming the old five-minute ceiling that the deleted constant used to set.
LLM_TIMEOUT_MS was validated as "a positive number", which lets two shapes through that then degrade into an immediate abort rather than a long one. A fractional value passes the check and floors to 0 in derive, which makes the dispatcher fall back to undici's default and setTimeout(abort, 0) kill every model call on the next tick. Anything past 2^31-1 does the same, because Node clamps the delay to 1 ms. Both now refuse at boot, which is what the wiki row already promised. Also pins the seam the three deleted constants used to occupy: the clients' abort deadline has to come from the configured ceiling. Nothing else in the suite would have noticed a literal coming back.
…ited A booking's linked expense price lives in reservations.metadata.price (written by BudgetService.syncReservationPrice). The booking form rebuilds metadata from its fields on every save and only carries transit / airtrail_ids over, so PUT /api/trips/:tripId/reservations/:id overwrote the stored metadata and the price vanished from the card until the expense was re-saved. ReservationsService.update() now carries price / priceCurrency from the stored metadata into an incoming metadata object that does not set them, as long as a budget item is still linked to the booking. A payload that sets price itself, a booking without a linked expense, and an explicit metadata: null behave exactly as before. Closes #2233
… the expense The carry-over was conditional on a budget_items row still being linked to the booking, which drops a real price in two shipped cases. The booking importer writes metadata.price unconditionally but only creates the linked cost when the Costs addon is on and the price is above zero, so an instance with that addon off has bookings carrying a price and no budget row at all; an MCP booking created with a price of 0 is in the same position. Both lost the price on the first edit. The condition is gone: a payload that does not name the key keeps what is stored, and naming it, null included, is taken at face value so a price can still be removed on purpose. What stops a price from outliving its expense is now the other end. deleteBudgetItem is the single chokepoint for all four delete paths, and it clears the mirror there, inside the same transaction as the delete. That also removes an ordering hazard the linked-row check had: the controller runs update() before it syncs the budget, so on an explicit clear the row still existed and the price would have come back a moment before it went. The predicate moves into reservation-metadata.ts so it can be read and tested on its own, and the update_transport tool description no longer promises that metadata is replaced wholesale.
Vector basemaps rendered Arabic, Hebrew, Persian and Urdu place names as isolated letters in logical order — "القاهرة" drawn as "ة ر ه ا ق ل ا" — because neither GL engine loads its optional RTL text plugin by default and TREK never registered one. Raster basemaps hid the problem, since their labels are drawn server-side, so it only became visible for everyone when the default basemap became a vector style. Both engine entry points now register the plugin lazily, so the payload is only fetched once a map actually paints RTL text and Latin-only users download nothing. The call is left with each engine because the two signatures genuinely differ: MapLibre takes (url, lazy) and returns a promise, Mapbox takes (url, callback, deferred). The plugin is bundled rather than loaded from a CDN: TREK is offline-first and the production CSP is script-src 'self', so a remote plugin URL would be refused by the worker that imports it. Bundling also puts it in the Workbox precache, so shaping keeps working offline. It needs a resolve alias because the package publishes only its ESM/WASM entry point through "exports" while setRTLTextPlugin requires the classic script in dist/ — the alias is shared by the build and test configs so the two cannot drift.
The guard in rtlText.ts had tests; the two lines that call it did not. setRTLTextPlugin appears at exactly two places in the client and no test touched either, so deleting a call, swapping an argument or dropping the deferred flag left the whole suite green. The engines are mocked because importing them for real builds a worker pool under jsdom, and the plugin URL is mocked so the payload never loads. The precache comment in vite.config.js was three releases stale, and it is the only record in the repo of what a service-worker install costs. A real build says 463 entries / 23,292 KiB, the plugin among them: it matches the js glob, so it is fetched at install by every user rather than on first RTL text as the branch description claims. That trade is the right one, since the alternative is a CDN the production CSP refuses and an unshaped map for anyone offline. Only the description of it was wrong. Measured against a real client build, which no CI workflow performs: none of them runs vite build or check:gl-split, so nothing here was covered before.
…flaking FE-PAGE-LOGIN-014 failed intermittently in CI while passing in isolation: twice in a row on unrelated PRs, each time costing a full run, and once on one of two runs of the identical commit. The takeoff overlay renders LoginWorld, which starts three requestAnimationFrame loops on mount. Under the load of the full suite that was enough to push the overlay assertion past waitFor's default timeout. Nothing in this file asserts on the animation, and LoginWorld has its own tests, so the file stubs it.
… new one on every save
resolveImportedPlace created the hotel's place through placesApi.create and
read .id off the response, but that endpoint answers { place }, so the id was
undefined: the accommodation was saved without a place_id and the new place
was orphaned. Because the planner's places state never learned about it, the
name match failed on the next edit too, and every save of the same hotel
produced another copy (five in a row on one trip).
Create the place through the store's addPlace instead. It unwraps the
envelope, so the accommodation is linked, and it pushes the place into
`places`, so a later save of the same hotel reuses it. The mobile sheet goes
through the same handler and is fixed with it.
The hook tests mocked placesApi.create with a bare { id } and therefore
never saw the envelope; they now assert on addPlace, and a new case saves the
same hotel twice and expects one place.
Closes #2243
The envelope fix routes the created place through the store, which offline
mints a NEGATIVE temp id: reservations have no offline write path, the
mutation queue rewrites temp ids in a URL but never inside another entity's
body, and day_accommodations.place_id carries a foreign key. So a
forced-offline save (the Settings toggle sets the flag while the network is
still live) reached the server with place_id: -1758..., failed the FK inside
the insert transaction and rolled the whole booking back into a 500 — where
dev merely saved it unlinked. Worse, the temp place is in `places` by then,
so every retry matched the same negative id and the booking stayed unsavable
for the rest of the session.
Offline the function now links nothing at all and skips the geocode too: the
booking cannot be written either way, so minting a place would only leave an
orphan behind on the next flush. The name-match arm rejects temp ids for the
same reason. `as never` is gone from the addPlace argument, and
placesApi.create finally declares its `{ place }` return type — an untyped
`r.data` is what let the envelope be misread in the first place.
…ctually sends
POST /api/addons/collections answers with the collection itself — the
controller returns the service's `Collection` straight through, and the e2e
suite pins that shape. The client declared a `{ collection }` envelope and
read `.collection` off it, so `createCollection()` resolved `undefined` and
handed back `null` every single time.
The caller needs that id. In the list editor and its phone sheet the id is
what the cover upload and `onCreated` hang off, so creating a list with a
cover picked in the same dialog silently dropped the image, the new list was
never opened or selected afterwards, and the retry guard that exists to stop
a failed cover step from minting a duplicate was dead. The list itself still
appeared, because `loadAll()` runs first — which is why this went unnoticed.
Nothing caught it because both test layers stubbed the shape the client
believed in rather than the one the server sends: the api suite's MSW
handlers wrapped the collection in an envelope, and the store suite's mock
resolved one too. Both now answer the way the controller does, `update()`
included — its declared type was wrong for the same reason, though no caller
reads its result.
The fix is on the client. That wire shape has shipped for as long as
Collections has existed and the server e2e test pins it, so changing that
side would be the breaking one.
… public language picker Three reports from the same week, all "the layout is in my way". #2247 — the Plan tab shows both side panels from 768px up, unconditionally. A Pixel 10 Pro Fold unfolded is ~860 CSS px, so 340 + 300 of panel left the map a 200px strip; those same widths became fit padding, shrinking the framing window to 140px and opening the camera two zoom levels out over half of East Asia. The floating POI/compass cluster is centred on the viewport at z-25, which at that width put it on top of both collapse tabs (z-1 inside z-20 containers) and on the Add Place/Activity button — and its pills set pointer-events: auto, so it ate the taps meant for them. The resize handles are mousedown-only, so a touch user could not narrow anything either. Between 768 and 1023px only one panel is open now, the day plan first, the places list one tap away — the Google Maps shape the reporter asked for. The collapse flags stay the wide-layout state, so a fold back restores what was there, and a marker tap reopening "both" no longer re-crowds the screen. The control cluster centres on the corridor the panels leave (the formula PlaceInspector and DayDetailPanel already use), the tabs finally have accessible names, the dead resize handles go, and a panel dragged to 520px on a desktop is clamped so the map keeps 360px without touching the stored width. computeMapViewport refuses to frame for less than a third of the box, so absurd padding can never pick the zoom again. #2248 — the public journey hero clips its own overflow to keep its decorative circles from widening the document, which also amputated the language menu at the hero's bottom edge: 8 of 23 locales, the rest unreachable. Both share pages inlined the same menu, so it is one component now, portalled to the body and anchored to its trigger the way CustomSelect is, with a height cap and its own scroll. It closes on Escape and on an outside click, which it never did, and `zh-TW` stops labelling itself 简体中文. #2249 — the note row's pencil/trash pill is absolutely positioned over the row's own reorder chevrons, and a coarse-pointer `opacity: 1 !important` pinned it permanently visible on tablets. On a mouse it was no better: the chevrons are revealed on hover, which is exactly when the opaque pill covers them, so note reordering has never been clickable on any pointer. The pill is gone; the row opens the note the way the phone shell already does, and delete lives in that dialog behind the same confirmation. A link in the note body keeps its own tap, on both shells. Closes #2247 Closes #2248 Closes #2249
SystemNoticeHost, BackgroundTasksWidget, and save-to-collection were gated only on login/register/password routes, so anonymous visitors to /shared/:token and /public/journey/:token still mounted them and hit system-notices/active with no session, logging a 401 on every visit.
Public journey pages ≥1024px render the sticky sidebar map immediately with photos, before the deferred fitBounds/setView lands, causing latLngToContainerPoint to throw "Set map center and zoom first."
The Leaflet mock had no getCenter, so the new guard bailed out of every draw and FE-COMP-JOURNEYMAP-046/047 went red. Give the mock a loaded map by default and add FE-COMP-JOURNEYMAP-050, which takes the view away for exactly the window #2254 hit: render must survive it drawing nothing, and the moveend listener must still put the thumbnail up once fitBounds lands.
The phone map filters down to one day and had no way back out. The chip rail can only swap one day for another, the second tap on the active chip is the day sheet, and the seed guard deliberately does not re-seed — so a day picked on the map stayed picked until you left the trip and came back (#2257). Desktop has had the deselect since the day header got it. Its own round control beside the rail, map view only: the plan timeline is single-day, so there is nothing to widen there, and the day sheet keeps the one route into it that also exists on the map. It toggles — the first press parks the day it came from and frames the whole trip, the second puts that day back with its route, or falls back to the focus day if a remote delete retired it in between. Leaving the map restores it too, since an empty timeline would be the worse answer.
…edirect hop Two reported advisories, and a third path neither of them covers. GHSA-7pqc-fj3c-9346 — the ntfy token fell back to the operator's whenever a request did not carry one, so an authenticated user who named their own server was handed the operator's decrypted token as `Authorization: Bearer`. The report covers `test-ntfy`, but the live send path in channels/builtins.ts does the same thing off a per-user `ntfy_server` setting: no test route needed, just wait for the next notification. Gating the route on `role === 'admin'` would have closed one of the two and broken the documented shared-server setup, where the operator supplies server plus token and users only set a topic. The invariant is the target, not the role: the operator's token travels to the operator's server and nowhere else, and that rule fits both paths unchanged. Scheme is part of the comparison — an http twin of an https server would put it on the wire in clear. GHSA-8mw6-xphx-886m — both transports checked only the first URL and then let the platform follow redirects on a dispatcher pinned to that first IP. Pinning does not help: Node skips the pinned lookup for an IP-literal host, so a 307 to 127.0.0.1 or 169.254.169.254 connected straight through. They go through safeFetchFollow now, which re-checks and re-pins per hop. safeFetch itself had the same shape and carried it to sixteen more callers, several with a URL out of a per-user setting, and one (pipeAsset) streams the response back — which would have made a redirect into an internal service readable rather than blind. It now routes through the same follower. The third: following by hand opts out of the platform's own rules, and both manual followers replayed the caller's init verbatim. undici drops `Authorization` across origins itself, so converting ntfy to safeFetchFollow would have re-opened the first advisory through a redirect. Credential headers are now dropped on an origin change (same-host http to https stays, self-hosted setups depend on it), and a 303 — or a 301/302 on a non-idempotent method — drops the body, so a plugin's client_secret is not re-POSTed at a host the first one merely pointed at. plugin-oauth pins maxRedirects to 0 like its OIDC twin, and the Google Places calls get the timeout none of their nine call sites passed.
The credential strip and the 303/302 body downgrade are new behaviour in a security helper, and src/utils sits on a 96% line ratchet that the untested branches took under.
toMatchObject({ method: undefined }) does not say what it looks like it says.
Five findings from auditing the release branch, none of them a blocker for it, all of them things it would have shipped with. The mirrored booking price only survived an edit when the form happened to fill metadata of its own. Both reservation forms and the phone sheet sent `metadata: null` whenever the form had no metadata key to send, and null clears the column outright — so restaurant, event, tour, parking, other, a hotel with no check-in times and a transport with no carrier details all lost the price again on the next save. They send the object now: it still clears what the form dropped, and the server carries the price across (#2233). Your share counted expenses nobody has paid for, while the balances right below it stopped counting them. The tile contradicted the panel it sits in (#2225). App.tsx kept its own list of routes that must not mount authed-only widgets, and it had already drifted from the one the response interceptor uses: this one named /public/journey/, that one covers all of /public/. It reads isAuthPublicPath now, so a route cannot be public to one and not the other. LLM_TIMEOUT_MS and TREK_PLUGINS_IGNORE_TREK_RANGE shipped in .env.example but not in the compose file most installs actually read. And a typo in the Russian offline copy: Завершёна → Завершена.
Two suites encoded the behaviour this branch changes: MReservationSheet asserted the literal metadata: null the forms no longer send, and the new Your-share case reached for a label that is a card footer rather than a heading.
…installed Two reports, both about an import that fails without saying why. #2262 — temperature: 0 sat in the shared request body, so it went out on every attempt including both retries. Reasoning models (the gpt-5 family) reject any explicit temperature, and the json_object retry fires on any 400, so the import spent two doomed requests and could not succeed at all. The parameter is now dropped once a server has explicitly said it will not take it. Not as a third link in the chain, though. A gpt-5 rejects max_tokens AND temperature, the API names one parameter per response, and it may name either first — a chain of one-shot ifs converges on only one of the two orders. The retries are a loop over what the server actually said instead: each remedy applies at most once and they compose. Detection is a phrase, not the bare word, because dropping temperature costs the deterministic sampling that small local models depend on, and an unfamiliar wording still falls through to the json_object retry exactly as before. NuExtract and Ollama are untouched: the first attempt always carries temperature, and it is the only one they ever see. #2261 — the bundled extractor is Debian trixie's 24.12.3, roughly eighteen months behind upstream, and the reporter is right that this is invisible: a stale extractor, a mistyped KITINERARY_EXTRACTOR_PATH and a genuinely unsupported provider all look the same from the UI, which is nothing imported and no reason given. The version now appears in the startup log and in a new admin-only GET /api/admin/system-info, and LOG_LEVEL=debug passes the extractor's raw stderr through — the JS ERROR and Invalid-result-type lines the filter drops are the only evidence that a vendor script is missing or failing. Capped at 200 lines, and opt-in because those lines can carry document fragments. Not bumping the package: the filter the reporter needs first ships in KDE Gear 26.08 and no Debian suite carries it, and sid's 26.04.3 wants KF6 6.22 and Qt6 6.10.2 against trixie's 6.6 and 6.8.2 — a partial upgrade of Qt6, KF6 and libc6 from unstable into the production image, on two architectures, that still would not fix the reported carrier. It comes for free with the base-image move after Debian 14.
The stderr and --version fixtures had literal line breaks in them, and the admin controller helper still built the controller with eight arguments.
The default basemap has been a vector style since 4.1.0, and maplibre-gl builds its WebGL context inside the Map constructor. A browser that refuses one throws from there, and VectorBasemap attaches the layer inside an async effect, so the throw became an unhandled rejection no boundary could see. What actually took the page down was the teardown. Leaflet registers a layer before onAdd runs, so the half-built one stayed on the map, and the bridge's onRemove calls this._glMap.remove() without asking whether there is one. That TypeError lands in a React cleanup, which is why the error appears on the way out of a map rather than on the way in, and why it reads as "Something went wrong" when a tab changes and "TREK could not start" when the route does. The browser is now asked first, once per page load, with a one-pixel probe that hands the context straight back. Without WebGL the basemap is OSM raster tiles instead of an empty map, and the maplibre chunk, about a megabyte, is never fetched. The teardown path is safe either way: a half-built GL layer gets a no-op stand-in so the bridge can finish its own cleanup, and detaching never throws, because unmounting a map must not fail just because its basemap did. Reproduced against demo.liketrek.com with WebGL switched off, and verified with the fix: 14 raster tiles drawn, no GL canvas, no error screen. Closes #2288
…in the form Six changes to the place picker and the card behind it, all of them about a panel saying less and meaning more. The TREK place index has no switch in the admin panel any more. It decides whether a search leaves the instance at all, which is a property of the deployment rather than a taste: an operator pins TREK_PLACES_ENABLED=false in their compose file, and it cannot be turned off from a browser by whoever holds an admin account that day. The switch never shipped, so nothing to migrate. The standing "Using OpenStreetMap" notice above the search box is gone. It was a sentence about configuration in the middle of a form about a place, and it answered a question nobody asked while leaving the real one open. Each result now carries a small mark instead: TREK, OpenStreetMap or Google. It sits on the row rather than above the list, because a list can be two indexes interleaved and a heading could only name one of them. The saved-places filters open across the full width of the row. Two of them sit side by side, only one can be open at a time, and list names were being cut at about ten characters for no reason. Three empty states use the mascot the rest of TREK uses. The place details panel no longer tells the reader to go and ask an administrator for a Google key, the saved-places list and the road trip rail no longer explain themselves in two lines of grey text. That needed one new mood, `sad`: `sleepy` reads as falling asleep, which is a different thing from finding nothing. The Kind of stop field is out of the place form. It is a road trip concept, and the road trip view already sets it from the stop's own number, where the choice is next to what it changes.
… too Five states, one shape. EmptyState grows a `fill` option: the stack already centres what it holds, but with no height of its own there was nothing to centre inside, so every one of them sat pinned under the header of its column. Optically centred rather than geometrically. The extra room goes underneath, which puts the mascot slightly above the middle: in a column you can scroll, dead centre reads as having fallen to the bottom the moment the list is short. The details column's own placeholder joins them, with the idle look rather than the sad one, because nothing being picked yet is a state and not a fault. Its line is shortened to fit on one: "Pick a result to see more", in all 23 languages. The corridor panel, the saved-places list and the place details all dropped `compact`, so the five now read at the same size as the road trip rail.
The places pool sits one click away from the plan, and a place you already know belongs to today had to go through the day picker afterwards to get there. With a day open, the add button splits: the left half still opens an empty form, the right half opens one that assigns the place to that day on save. The plumbing was already there. placeFormDayId has assigned a freshly created place to a day since #1998; nothing in the places rail ever set it. The main label shortens to "New place" while the split is out, so both buttons fit side by side in the rail without either one truncating. The second button stays mounted and collapsed rather than unmounted, so it has something to animate out of: width, padding and opacity over 180ms, and nothing at all under prefers-reduced-motion. Collapsed it is also out of the tab order and hidden from screen readers, because a zero-width button is still focusable otherwise. Both columns of the place form are 320px now. Two panels of different widths flanking one form read as a mistake rather than as a hierarchy, and neither of them is the more important one.
…ed add The satellite toggle sat on an offset that included --day-panel-h, so it hopped every time the day panel was collapsed or expanded. On the desktop map that panel is a centred card between the sidebars and never reaches the pill at the left edge, so the lift only applies on a phone now. In the places rail the two add buttons split the width evenly once a day is open, and every button drops to its icon when the rail is dragged narrow. The filter tabs span the rail and keep their own text widths. The count is a badge across the rail; the search field got the outline the controls around it use. An empty day offers a button that opens the place form pointed at that day instead of only stating that nothing is planned. The day is cleared again when the form is opened to edit, which it was not before. The trip tab bar no longer carries a native title repeating its own label; the full name moved to aria-label for the tabs whose label is abbreviated. Three maps tests drove the index switch through app_settings, which no longer holds it, and now move TREK_PLACES_ENABLED instead.
A solid outline and a whisper of accent instead of the dashed grey placeholder it replaced, so an empty day reads as an offer rather than as something switched off, without competing with the day rows around it.
Attaching a receipt no longer lets the budget domain delete one. Trashing the file was reachable from the REST route, the MCP tool and the plugin RPC, none of which check file_delete, and a receipt id is any file on the trip, so anyone who could edit an expense could trash a document they were not allowed to touch. A concurrent edit could do it by accident: the client sends the full set of receipt ids from a snapshot taken when the form opened, so saving an expense someone else had just attached a file to moved that file to the trash. Removing a receipt now removes the link and nothing else. The link row is treated as shared, because it is. One row can tie a file to a place and to an expense at once, so the sync clears only the receipt half and leaves the rest, the foreign key is SET NULL rather than CASCADE, and a receipt already in the trash keeps its link so restoring it comes back attached. The upload and its rollback moved into one module both shells call. It was copied into six places, and all six rolled back with the permanent-delete route, which only accepts a file that is already in the trash and therefore answered 404 every time: a failed save left every uploaded file on the trip, attached to the expense, and the next attempt uploaded another copy. It now trashes first, keeps the pending files for the retry, and says so when a file could not be removed instead of swallowing it. Escape in the receipt viewer no longer closes the expense form behind it, the download fallback uses a real accent class rather than a utility that does not exist, and the upload indicator is actually switched on. Tests: the receipt link behaviour on both service paths, the four new branches in the files service, the upload module, and the viewer.
listFiles projects linked_budget_item_ids now, so the harness table it selects from needs budget_item_id or every list call in that file answers 500. Same gap the collab harness had.
…nner buttons The User Defaults and Notifications tabs were one narrow column of cards on a page that has no max width, so the right two thirds stayed empty while the page scrolled for ages. Both now use the two-column grid the Settings tab already has, grouped by subject rather than by height. User Defaults splits into "Default User Settings" (colour mode, units, time format, currency, booking codes) and a "Map" card holding template, keys, routing base, preview and engine. The columns are deliberately uneven — the left one never needs more than its widest option row, the right one holds four fields whose values are 60-character tile URLs. Notifications keeps the three channel switches together on the left, because they all write the same notification_channels list, and puts the operator's own targets on the right. The admin preference matrix moves in with them instead of running full width underneath: it decides which of those channels each admin-only event goes out over. Its label column gets minmax(0,1fr) so it wraps rather than overflowing in the narrower column. The transit provider was the one native <select> left on the Settings tab. The option list of a native select is drawn by the OS, so it followed neither the colour scheme nor the radius nor the user's text-size setting; it now uses the CustomSelect the rest of the app uses, and skips the request when the option already selected is picked again. In the update banner, "View on GitHub" was a size smaller than "How to Update" on every axis — 12px against 14px, a 14px icon against 16px, less padding, and 2px shorter. Two buttons side by side read as one group, so that reads as a mistake rather than as hierarchy. Same box now; the fill, the colour and the weight still mark the primary action.
Adds a Collab Links panel (add/pin/delete) with translations in every locale so tab and button labels are not raw i18n keys, chat image attachments (up to 4), and native fullscreen on plugin iframes. Includes Zod request DTOs and CollabPanel coverage for the Links tab.
The chat image filter believed the Content-Type the client put on the part. The stored name keeps the extension of the name the client sent, and the download route derives what it serves from that extension and sends it inline, so pwn.html declared as image/png came back as HTML on our own origin. The filter now checks the extension as well, against the blocked list and against the types it claims to accept, because a per-route filter fully replaces the module-level one it looked like it was adding to. The multipart message route also kept its guard decorators. Guards run before the interceptor, so a refusal went out while the client was still streaming up to four images and the socket died as ECONNRESET rather than carrying the error. The checks moved into the handler, where the other upload route in this file already does them, and a test pins both refusals. The three collab:link events were missing from the shared registry, which the server typecheck refuses, and the admin Links switch could never be saved because the Zod contract had no key for it. Both are in now. Migrations moved to the end of the list and the column add uses the pragma guard rather than a caught duplicate-column error. The message-required string keeps its old wording for a request without a file part, so a client from before this route took images sees what it always saw.
…inks The desktop layout rendered notes and links as a pair and dropped whichever of the two was on its own, so turning the new Links feature off on an otherwise default install made the Notes panel disappear. Both rows are now split by kind and rendered only when they hold something, and one function decides what a panel id renders, because the branch that lost a panel did so by not listing it. The message and its attachment rows are written in one transaction. The caller has already committed the image bytes by then, so a message that landed without them left those bytes with nothing pointing at them and nothing to sweep them. Deleting one now drops the blobs after the rows are gone rather than before. A body the Zod pipe rejects used to leave up to four spooled images on disk: pipes run after interceptors, so multer had already written them and the handler's own cleanup was never reached. An interceptor removes them on any failure before the handler, which also covers every other schema mismatch, and the length check that could never fire is gone. Adding, pinning and deleting a link now says something when it fails instead of raising an unhandled rejection, the list has real types, and the rejected-image toast is translated in all 23 locales rather than shipping English everywhere. The picking, previewing and clearing of chat images moved into one hook both shells use: it was copied into two, and both rebuilt every preview on each change and leaked an object URL per image per pick. Tests: the four link routes end to end with the validation pipe the harness was missing, the image filter against the disguised-extension case, and the layout combinations that drop a panel.
The URL check used `new URL(...)`, and this package compiles against lib ES2022 alone so that it carries neither DOM nor Node globals, which is why the shared typecheck refused it. `z.url()` plus a protocol test does the same job with names the package actually has; z.url() on its own would take mailto: and javascript:. The three collab:link events also needed their fixtures and their place in the pinned inventory count, the guard that stops the fixture map drifting from the registry.
…columns The three collab:link events were broadcast and landed nowhere: the links panel held its own list and never listened, so a link somebody else added showed up on the next reload. It listens now, the same shape the notes panel uses, and the events are classified in the client's WS policy, which is the ratchet that caught this. Four e2e harnesses build their own trip_files and one asserts the collab feature set, so the new message_id column and the new links flag had to reach them too. Two MCP unit tests pinned the old four-key feature object.
The chat images never reached a test: committing them to storage, taking them back out when a later one fails, and dropping them again when the reply target turns out to be gone. The interceptor that removes the spool when a request dies before the handler had none either, and it sits in a domain pinned at 99% function coverage. Both filters are pinned too, including the disguised extensions that were the point of changing them.
`ReturnType<typeof of>` resolves against the zero-argument overload, so the
stub only accepted `Observable<never>` and SPOOL-001 could not hand it the
`of('ok')` it needs. tsc -p tsconfig.tests.json broke on it, which sank the
server preflight job and with it every coverage job that needs it.
Docker Scout turned the release PR red overnight on advisories published after the last green run: multer < 2.3.0 CVE-2026-82333, CVE-2026-77078, CVE-2026-77037 nodemailer < 9.1.0 GHSA-2x7j-588g-ccc2 Both are patch-level resource-exhaustion fixes with no API change — multer 2.3.0 carries the same four dependencies as 2.2.0. multer resolves through the root override (@nestjs/platform-express asks for 2.1.1), so the range there has to move with it, and the server's own override list beside it.
…hone The desktop form has offered stops between pick-up and return since the road trip release (#1797). The phone sheet never knew about them: its car branch rebuilt the endpoint list from from/to alone, and because the server replaces every endpoint row of a booking, saving there silently dropped whatever the desktop had set. Its car branch now reads the stop endpoints back into an editor and writes them out again, the same shape TransportModal uses: location, time, reorder arrows, remove.
The schedule only ever walked forward, so a stop before the first pinned time stayed blank. That is the wrong way round for how people plan: the museum opens at ten, so when do we have to leave? Pinning the time on the second stop and reading the first one's departure off the chain is the whole point of pinning it. The chain now runs both ways — forward from the first known arrival as before, backward for everything ahead of it, departure being the next arrival minus the drive and arrival that minus the stay. It stops at a leg that never routed rather than inventing a duration, and when working back crosses midnight the whole chain shifts up a day so the earliest stop stays on day zero and the boundary is drawn between the two stops instead of under the first one.
…ause Three things the rail was quietly getting wrong at a day boundary. A stop reached later than the time set on it showed a warning only when it carried a number. The schedule restarts its chain at any anchor, whatever kind of stop holds it, so the finding was computed for a fuel or charging halt too and then thrown away. Both branches now draw it, from one component, and it sits in the badge row beside the stay rather than on a line of its own. The midnight crossing was a small pill floating in the middle of the rail, which read as a badge belonging to the stop underneath. It is now a band across the full width that breaks the dashed line instead of letting it run through, with the mascot asleep in the gap. Both its animations stop under prefers-reduced-motion. And the stay dialog said 'leave 01:00' for a departure the chain places on the next day, because the clock wraps modulo 24 h. It now carries the same day marker the rail has always shown beside such a time.
'Wähle aus, was du brauchst, und suche' is a word-for-word rendering of 'Pick what you need and search' that no German speaker would write; 'Wieder ein Ziel' is not a sentence a button can carry; and 'Alle Leistungen' reads as services rather than charging power in a dropdown of kilowatts.
The category pill was switched off whenever road trip mode was on, which left the corridor search as the only way to find anything. The two answer different questions though — 'what is along this drive' against 'what is in view right now' — and looking for a hotel at tonight's stop is exactly the second one. Furkot shows accommodation at a stop for the same reason (disc #1797 thread). Both searches now draw together. A place found by both appears once, as the corridor knows it, because only that copy carries how far along the drive it sits, and the merged list keeps its reference while nothing changes so the map does not rebuild every pin on each render.
Every route is calculated in the browser against the public FOSSGIS hosts, so the server never sees a routing request and nobody has a number: not how many a real trip costs, not how big they are, not how much already goes to a self-hosted engine. Deciding whether TREK could run its own router without that number is guessing at the bill, and the counters have to be collecting before the question comes up. Counters, not a log. A row is a day, a profile, a request kind and an engine kind with totals on it: how many requests, how many waypoints, roughly how far, how many were refused. No query, no coordinate, no route, no user, no trip. Nothing leaves the instance: its own database holds them and its own admin reads them at GET /api/route-usage/summary. The client tallies in memory and flushes in batches — on a timer, past a threshold and when the page is hidden — because a counter that cost a request of its own would measure the load by adding to it. Routing all funnels through one fetch in RouteCalculator, so that is the only place instrumented. On by default and switchable off with route_usage_enabled, unlike the shadow log next door: that one stores what people typed, this one stores four integers about a day.
The map has narrowed to the selected day on the Planned filter since #2024. The pool beside it did not, so a trip with 55 planned places read 55 in the tab and drew five pins on the map, with nothing anywhere to say the two were answering different questions. Reported twice in the same thread as the map being broken: you pick a day, open Places, tap Planned, see everything you ever planned, and the map shows a handful of it. The list and the tab count now use the same set the map does. Unplanned deliberately stays trip-wide: a place assigned to some other day is planned, whichever day happens to be open. A note under the tabs says so and closes the day when dismissed. It sits there rather than over the map because a chip on the canvas is out of reach on a phone, which is where this was reported from.
The imagery toggle has been there for a while and only ever worked on Leaflet, so anyone on MapLibre or Mapbox — which is the default since the vector basemap landed — had a switch they could not see and a view they could not reach. Leaflet swaps its whole tile layer for the imagery. A GL map cannot: its basemap is a style with dozens of layers in it. So the same tokenless ESRI source goes on as a raster layer of its own, inserted before the first layer TREK draws, which leaves the route, the pins and the tracks on top and legible. Switching back hides it rather than tearing it down, so going to and fro does not re-fetch a tile. Built on demand rather than once at load, because a basemap change rebuilds the style from scratch and takes every source with it; the styledata event is subscribed for the same reason. The switch reads the setting the Leaflet map already stores, so the choice survives an instance changing renderer, and it sits in the same corner with the same offsets.
Typing "Tokio station" from Tokyo suggested a weigh station in Ritzville and a
station in Tuxtla Gutiérrez, and typing "Tokyo Station" put Japan in seventh
place behind Markham, Arlington, Calgary, Milano and Melbourne. The station
itself was never in the list.
The service has had the answer all along. Its OpenStreetMap layer carries 14.6M
travel places with every name form each is known under, including the exonyms a
traveller actually types, and it is not the same data as the index: Overture
holds businesses, so a search for a station finds the shops inside it. The layer
returns "Tokio Hauptbahnhof" for the German spelling and 東京駅 for the Japanese.
It was never asked. `sources` defaults to the index alone and trekPlacesSearch
never sent the parameter, so no caller here has ever seen a layer row.
Sent on the autocomplete path only. The explicit search asks Nominatim in
parallel and would get the same OpenStreetMap places twice; this path asks
nobody else, because Nominatim's usage policy names autocomplete as
unacceptable use. So the layer is not a second opinion here, it is the only
place those names live.
A layer row has a different shape, and that shape is why this is more than one
parameter. It has no `gers` and no address block, and its id reads
`osm:node/9712313` where this file resolves `node:9712313`. Passing it through
untranslated would hand the client an id getPlaceDetails does not know, and the
failure would land after the user had already picked the suggestion. The
secondary line falls back to the local name, which is what is written on the
building and more use under a translated label than an empty line.
Measured against the service before the change, same query, same coordinates:
"Tokio station" index only -> Tokio Weigh Station, Ritzville US
with layer -> Tokio Hauptbahnhof in second place
"東京駅" index only -> six restaurants inside the station
with layer -> 東京駅 itself in the list
…ething The satellite toggle has worked on Leaflet for as long as it has existed and did nothing at all on a GL map, which is the default renderer since the vector basemap landed. Not for want of the layer: connect-src never named server.arcgisonline.com. Leaflet asks for a tile as an <img>, and img-src waves every https: host through, so the gap could sit there unnoticed for as long as Leaflet was the only renderer that showed the imagery. A GL map reads the raster through fetch to hand it to WebGL, and that lands on connect-src instead, where the host is missing. The browser refuses it in the document, which is the worst shape a failure can take here: the switch flips, the layer goes on, the source is there, and no tile ever arrives. Nothing in the app can see it either, because a CSP refusal is not an error the fetch can catch. The prefetcher asks for the same tiles the same way, with mode 'no-cors', which relaxes CORS and not CSP, so the imagery was never cached for offline use either. Same shape as #2180, which added the other two raster presets for exactly this reason and did not reach this host.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Features
Road trip
Places
TREK_PLACES_ENABLED=falseon the instance, which leaves search on OpenStreetMap plus Google where a key is set.TREK_PLACES_URLpoints an instance at a self-run copy, and a Google key still answers where both come back empty and stays the only source of ratings and photos. (feat(maps): search places through the TREK Places API #2258, Say where a place result came from, and stop asking questions in the form #2295)update_place,bulk_update_placesandcreate_and_assign_placetake it, and the update tools accept an explicit null to turn a service stop back into an ordinary place.Costs
Collab
Instance settings
route_usage_enabledsetting; an admin reads them at/api/route-usage/summary. (Road trip follow-ups and routing usage counters #2304)probesinvalues.yaml, so a cluster with slow storage can raise the initial delay instead of watching the pod restart in a loop. The defaults render exactly the manifest the chart produced before. (feat(helm chart): make health probes configurable in Helm chart #2245)Fixes
OVERPASS_TIMEOUT_MSnow defaults to 25000 ms.Internal
FE-MOB-MSTOR-013stops racing itself: it read the migration POST body straight after the save toast, which only ever passed while that effect happened to flush first. (Follow the new default dock through the mobile nav suites, and stop one storage test racing itself #2287)