mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
4225d883c01df4d29aaa1d56bcfdf55cd8a45340
409 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4225d883c0 |
feat(mcp): half days, comp days, and the vacay plan settings
The toggle took a date and nothing else, then hardcoded a full vacation day for the calling user, so a half day, a comp day or a day logged for another plan member had no expression. School holidays needed the plan flags that switch them on, and the region list that makes them usable. That list needs care: a group region is stored as COUNTRY|group:CODE and the reader treats a bare code as a subdivision, so the tool now returns the string add_holiday_calendar actually wants. Belgium and the Netherlands are the two countries this affects. |
||
|
|
fed562d564 |
feat(mcp): bags, quantities and the sharing tiers of a packing item
Bag assignment was reachable only while bulk-importing a list, and by bag name, never for an item that already exists. Quantity, weight and the private flag were not reachable at all, and the whole three-tier sharing model was invisible: an item made by a tool was always Common. |
||
|
|
e0169388d3 |
feat(mcp): uneven expense splits, and the fields update was missing
Historical costs are rarely split evenly, and every budget tool took only a list of member ids, which the service stores as a null amount, meaning an equal share. members[] carries what each person owes instead. A custom split has to reconcile before it is persisted, so the tool refuses one whose shares do not add up in whole cents. Two ways round that check needed closing with it: the write path drops a payer who is not on the trip and then derives the total from what survived, and an empty payer list means a total of zero rather than the previous one. Both would have certified a split the stored row does not satisfy. Also on update, which had none of them: currency (so the FX freeze is no longer a no-op), expense_date, and a settlement currency, plus place_id on create so a museum ticket can point at the museum. |
||
|
|
f85a87e6d0 |
feat(mcp): reorder days and insert one mid-trip
An imported itinerary landed in whatever order the assistant appended it, with no way to fix it afterwards: create_day only ever appended, and days.service.insert was reachable from exactly one place in the server, the REST controller. Because both halves were missing, inserting a day into the middle of a trip could not be expressed at all. |
||
|
|
f1bbd94f2b |
feat(mcp): booking link and end time on a reservation
url is persisted on create and update and rendered as a link by the planner, and reservation_end_time is what makes a dinner run 19:00-21:30 rather than just starting. Neither was in a tool schema, so a booking written by an assistant lost both - which for an imported itinerary is every confirmation link it came with. url validates through reservationUrlSchema from @trek/shared, the same refinement the REST route applies, rather than a second hand-written one. |
||
|
|
aa5002b2ed |
fix(mcp): keep transit out of the transport create enum
The transport form's picker has nine entries, not ten: a transit booking carries a provider itinerary in metadata.transit and create_transit_journey is what writes one, so letting a caller hand-make a bare transit row produces a shape the transit UI does not expect. Splits the one list in two - what may be asked for, and what counts as a transport for the update gate - so a stored transit booking stays editable through update_transport, which is where it was already reachable. Also reports the ids set_reservation_travelers dropped instead of returning a plain success with a shorter list. |
||
|
|
86c0c197db |
feat(mcp): set who is travelling on a booking
Reads have hydrated a booking's travellers since #1517; there was no way to write them, so an imported booking could not record who was on it - and creating a guest is only half an answer if the guest cannot then be attached to the flight they were on. Mirrors PUT /api/trips/:tripId/reservations/:id/travelers. The roster filter stays in the service, so an off-trip id is dropped rather than attached and the tool cannot be used to enumerate users. |
||
|
|
4b21a70ab2 |
feat(mcp): add, rename and delete trip guests
add_trip_member resolves an existing account by username or email, so a travelling companion who never signed up could be put on a trip in the planner and not through an assistant. A guest is assignable to budget splits, packing, to-dos and day participants like any member, so leaving them out of an import loses who was actually there. Mirrors the owner-only /api/trips/:id/guests routes, including the trip-scoped guard that keeps one trip's owner away from another trip's guest and off a real member's user row. |
||
|
|
34522ad3bc |
fix(mcp): accept every transport type the planner offers
create_transport and update_transport took flight, train, car and cruise. The picker in TransportModal has ten modes and ReservationsPanel renders all of them, so a bus, a ferry or a taxi could be planned in the UI and not through an assistant - which is where importing a travel history from another service runs aground. Both lists now come from one constant in the picker's order rather than four literals repeated across two schemas, which is how they drifted. legs[] stays on flight and train: the form writes metadata.legs for those two and for nothing else. |
||
|
|
92575c0d49 |
@
fix(import): stop reading tickets as lodging, and stop scroll escaping a dropdown Two v4.0.0 reports. The file importer classified nearly every transport document as a hotel. Its keyword table tested lodging second, on a group that included "check-in" and "check-out" — words printed on almost every ferry, train and bus ticket there is — so a ferry ticket came back a hotel, and the review then handed the user the booking form, whose six chips contain no transport type at all. That is where "everything ends up as other" comes from: it was the only honest pick left. Transport is tested first now, the lodging pattern asks for a word that names lodging, and the rental pattern asks for a rental rather than for a pick-up time that ferries and shuttles also print. Two more in the same function. The flight path forces its type on everything it returns, and it claimed any document with two letters followed by digits — which is what a German rail number looks like, so an IC ticket was imported as a flight without the keyword table ever running. It now stands down when the document already names another kind of transport. And a type neither form can express opened the booking form regardless; it now follows the tab the import was started from, while a type either form knows still wins over the tab, since one PDF routinely holds a flight and a hotel. An unreadable type reaching the transport form arrives as transport_other rather than as a flight: a wrong flight looks right enough to be saved unnoticed. The other report: a dropdown inside a sheet scrolled the page instead of its own list. The panel is portaled to document.body and positioned fixed, so its scroll chain runs to the viewport rather than to the sheet it looks like it belongs to, and nothing stopped it. Three portaled lists carry overscroll-behavior: contain now — the select, the navigation menu (which reaches the phone through the place sheet, not only the desktop inspector) and the phone day picker. @ |
||
|
|
88c34f6415 |
@
test(calendar): cover the window paths the split added The domain branch ratchet went from 93 to 92.3: the new hand-over code brought branches with no case behind them. Three cases and one simplification, no threshold touched. Covered: a split rental whose endpoints carry no IANA zone and fall back to their coordinates; a bare end clock, which resolves against the start date and therefore does NOT split; and an unsplit rental with a day but no clock, whose hand-over has nothing to place and correctly emits nothing. The metadata parse now happens once, inside describeReservation, rather than three times. The two extra copies were only there to hand `meta` to it, and one of their arms is unreachable from SQLite, where the column is always text. @ |
||
|
|
bbe4d62c90 |
@
fix(export): print the reader clock format, and split window bookings in the feed Three v4.0.0 reports that all come down to a surface printing what is stored instead of what the reader asked for. The PDF (#2066) is assembled as an HTML string outside React, so no hook could reach it and no time format was ever passed in. It printed the column: four clock sites, all 24h whatever the personal setting or the admin default said, and a place time typed by a 12h user came out as "3:00 PM" for a 24h reader. Both export entry points now hand the format over the way they already hand over the locale, and the four sites go through formatClockTime. The journey editors (#2067) used a native <input type="time">, which paints 12h or 24h from the browser locale and cannot be told otherwise by any attribute. Every other time field in TREK has used CustomTimePicker for exactly that reason; the journey editor, its phone sheet and the phone place sheet were never migrated, so the same place form obeyed the setting on desktop and ignored it on a phone. The picker gained a disabled prop so the read-only journey sheet keeps the affordance the native input gave it. In the subscribed calendar (#2068), a booking that is a window rather than an activity read as one block across its whole range: a week of airport parking sat on all seven days. Both hand-overs now stand on their own — an hour each, carrying what the block used to say — and the block goes. Only when the two ends fall on different days: a same-day booking is one sitting, and a rental with only its pickup geocoded keeps the block, which is the only thing holding the return time and its zone at all. Rental cars were missing outright, because the planner writes reservation_time = NULL when its optional time pickers are left blank while still setting day_id, and nothing in the exporter ever read the linked days. It reads them now, as the last resort, for every type — so a hand-typed bus or train shows up as well. @ |
||
|
|
f15a124016 |
@
fix(dashboard): fix the upcoming widget dropping bookings, and give it the stays back Three things, all in the same widget. reservation_time holds two shapes. The booking form writes the datetime-local YYYY-MM-DDTHH:MM; day-anchored rows, the demo seed among them, hold a bare HH:MM. The query compared the column against an ISO timestamp as a string, where ":" sorts above "2": every booking from 20:00 on passed the filter and everything before it silently vanished, and the order ran off the same column. On the demo that is why the Broadway show shows and the ferry does not. The query now separates the date from the time before it filters, so a value with no date falls back to the day it hangs off instead of being read as one, and the order is by day and clock. The demo seed writes full timestamps, which is the shape every real write path already produces. Hotels came out of this list because a stay covers a range and a week in one would hold a slot for the whole week. Arriving and leaving do not: they are moments, and they are the two worth reminding someone of. Check-in and check-out now appear as their own entries, named after the stay place and falling back to the linked booking. The stay itself stays out. The two carry the accommodation id, so the list key had to take the type with it. An unconfirmed booking looked exactly like a confirmed one. The status was already in the payload and is now a badge, on both the desktop tool and the phone widget. @ |
||
|
|
1f66ef1d32 |
fix(maps): let users and admins enter a CARTO API key (#2060)
CARTO stamps an "API KEY REQUIRED" watermark onto every basemap tile fetched without a key since 26.08.2026, and both CartoDB presets plus the built-in fallback are CARTO. The key is free and needs no account, so add it as a setting rather than dropping the provider. carto_api_key sits next to mapbox_access_token: per user in Settings/Map, instance-wide under Admin/User Defaults, encrypted at rest, injected from CARTO_API_KEY on managed instances. Unlike Mapbox it never switches the map provider, since a raster key says nothing about the renderer. The key is appended as ?key= at render time instead of being stored in the template, so a saved URL stays portable and survives a rotation. That is also what reaches the four maps that pinned CARTO and ignored the user's tile setting: atlas, collections, the public trip share and the public journey. The two share pages read it from their token-gated payload, the same way baseCurrency already travels. Also lifts the release notice above page chrome. Its overlay sat at z-index 50, under the Vacay mode toolbar at 61, so the dialog was neither on top nor modal. Adds a six-step layer scale to index.css and moves the three notice shells onto it. Closes #2054 Closes #2052 |
||
|
|
521e52c853 |
fix(collab): harden the link preview fetcher (#2058)
The preview endpoint takes a URL from whoever is typing in a trip chat and fetches it from the server. Trip access was added to it earlier; this is the rest of what that route needed. The SSRF guard had a hole under it. `isAlwaysBlocked` knew `::1` and `0.0.0.0` but not the IPv6 unspecified address: `::` starts with a colon, so the `0.` check never saw it, and connecting to it lands on loopback. `http://[::]:9200/` therefore passed the guard and came back with the target's title and og: tags scraped out of the body — an exfiltrating read of anything listening locally, from any account. `[::0]`, `[0:0:0:0:0:0:0:0]` and `[::ffff:0:0]` are the same address in other spellings and behaved the same way. The two `::ffff:`-prefix regexes had the same shape of gap: they matched the dotted spelling of loopback and link-local, so `::ffff:7f00:1` walked past them. Both checks now decide on the expanded hextets, which covers every spelling at once, and fe80::/10 is read as ten bits rather than as the four characters 'fe80'. This is in ssrfGuard, so every caller gets it, not just collab. The scrape could be turned into a denial of service. Each og: regex had two unbounded `[^>]*` runs separated by a literal, so a document with no `>` in it made the engine rescan the remainder from every `<meta` it passed — quadratic, and the byte cap admits half a megabyte. Measured on this build: 240KB of '<meta ' took 58 seconds, and Node has one thread, so that is the entire server, health check included, on one request. Bounding the runs at 512 characters makes it linear (540KB now takes 1.4s); no real attribute list comes close to that. The five-second timeout only covered the headers — `clearTimeout` ran before the body was read — so a server that answered fast and then dripped one byte every few seconds held the handler, the socket and the per-request undici agent open indefinitely. The byte cap counts bytes and at that rate would never reach one. `AbortSignal.timeout` covers both halves, the agent is closed in a `finally`, and the three bail-outs that used to return without touching the body now discard it, which is what cappedFetch's helper is there for. The rest is smaller. Nothing metered the fetches, so an account with a trip of its own could drive arbitrary outbound requests in a loop; there is a budget of sixty new fetches a minute per user now, charged per outbound fetch rather than per request. Previews are cached for ten minutes and simultaneous askers for one URL share a single fetch, so the client's habit of requesting a preview per rendered message — up to a hundred at once, with no debounce — costs one fetch per new link rather than one per render. Refusals answer with one constant reason instead of the guard's own: it distinguishes "could not resolve" from "private address", which together map out the server's internal DNS for anyone willing to guess hostnames. A body whose declared type is not markup is dropped rather than combed for og: tags, and an og:image that is not an http(s) URL is not passed to the client, which puts it straight into an <img src>. Deliberately no `@RequirePermission` on the route: the client asks for a preview while rendering a message or a note, never while composing one, so it belongs with the GET siblings. Requiring collab_edit would blank every preview for members of a trip whose owner narrowed that right while leaving them able to read the chat, and it would not stop the abuse it looks like it addresses — the permission check passes for a trip's owner at every level, and anyone can create a trip. |
||
|
|
a3ad53b3bf |
fix: close the backtracking patterns SonarCloud's profile turns up
The move to SonarCloud brought a wider rule set with it, and 23 more regexes that can be made to hang on a crafted near-match. 21 are fixed here, each proven equivalent to the pattern it replaces by brute force rather than by reading. Two of them are reachable across users, not just by yourself. A reservation endpoint name is free text any trip member types, stored and then read back in every other member's browser while the map draws its labels, and the pattern over it takes 2.3 seconds on a 32k paste - so one member could freeze the map for the whole trip. The mobile transport sheet reads the same field. Both now scan instead of backtrack, and both were matched against the desktop twin the earlier sweep had already fixed rather than given a third spelling. The markdown link stripper needed the same treatment as the tag stripper before it: the label class cannot be disambiguated in the pattern, because it matches an opening bracket as well, and narrowing it changes what "[a[b](c)" produces. It is a scan now, with all four of the regex's quirks intact. Also here: the two encoding warnings the scanner reported. A decorative comment line in JourneyDetailPage.test.tsx had picked up a replacement character somewhere along the way, and calendar.service.test.ts asserts on one deliberately - that one is now written as a U+FFFD escape, so the file no longer carries the raw bytes while the test says exactly what it said before. Seven findings were deliberately not changed and are flagged in SonarCloud with the reasoning. Five are false positives - patterns with no overlapping quantifier pair, where the only cure would change how nested brackets parse. Two are marked accepted rather than dismissed, because they are real: the readme check's heading pattern has a proven linear replacement that cannot land until the registry's copy of the same script can move with it, and `export let JWT_SECRET` is what makes an admin's key rotation take effect in-process instead of silently waiting for a restart. |
||
|
|
62b13a278e |
refactor(common): one shared comparator instead of three inline copies
The coverage ratchet caught something worth keeping: an inline `(a, b) => (a < b ? -1 : a > b ? 1 : 0)` is a new function with four branches at every call site, and two of the three in src/nest/common sit on error paths that a passing test never walks. Three restatements of the default sort order were enough to pull the domain under its pinned thresholds (functions 97.8% against 99, branches 87.92% against 89). Writing tests to walk those error paths would have been solving the wrong problem. byCodeUnit says the same thing once, is covered once, and the callers read better for it. Its test asserts equality with a bare `.sort()` rather than hand-written expectations, since "identical to the default order" is the whole contract. It also pins what byCodeUnit is NOT: `['Zelt', 'Übernachtung']` keeps that order, where localeCompare swaps it — so the next person reaching for a comparator on a list a human reads picks the other one. |
||
|
|
071f516c0f |
refactor(llm-parse): use the shared tag stripper instead of a second copy
Two passes over the ReDoS findings arrived at the same scanner independently — one replacing `/<[^>]+>/g` in the KML importer and the Commons metadata helper, the other in the mail/HTML text extractor. The algorithm is the same down to the edge cases, so the extractor now imports common/stripHtmlTags rather than carrying its own. Checked before folding them together rather than assumed: the two implementations were compared over 2,441,406 generated strings and agree on every one. The shared helper takes the replacement string as a parameter, which is the only thing that differed — the extractor substitutes a space, the KML importer nothing. Two cases the shared helper's own test covers only in the abstract get a concrete one here, since this is where the input is a file somebody uploaded: an empty `<>` stays literal, and a `<` inside a tag is content, so `3 < 4 and <b>5</b>` strips to `3 5` rather than keeping the first bracket. Both are the old regex's behaviour, which is the point. |
||
|
|
1ad2e92142 |
fix: make the app keyboard-reachable, and close four DoS-able patterns
The bulk of what SonarQube still had open on the reliability side, read site by
site rather than swept. Two clusters are worth calling out because they are
defects, not lint.
**Four regexes could be made to hang the process.** The worst is
`/.+@.+\..+/` in the forgot-password path: three unbounded `.+` that all
overlap, unanchored, on an endpoint that needs no login and whose schema is a
bare z.string() under a 100kb body cap. A thousand '@' characters had not
finished after 100 seconds — one request pins the event loop, and Node has one.
EMAIL_REGEX in auth.helpers is the same shape on register and profile update
(310ms for 8kb). The two tag strippers over an uploaded KML description and over
Commons metadata took 0.52s on a 16k run of '<'. Each rewrite was proven
equivalent by brute force against the original — millions of strings for the
coordinate patterns, 671,876 for the tag stripper — because a subtly different
regex on a parsing path is worse than the finding.
`<[^>]+>` could not be fixed in the pattern: `[^>]` matches `<` too, so the only
unambiguous form changes what happens to malformed markup. It is now a scan in
common/stripHtmlTags.ts, quirks included — `<>` is left alone, `<a<b>` is one
tag — with the equivalence pinned by a test that compares against the old regex
over every string up to length six.
**And roughly 500 places could only be reached with a mouse.** Clickable divs
became real buttons where the markup allowed (52 of them), took
role + tabIndex + Enter/Space where they had to stay divs, and modal backdrops
took role="presentation" — the honest role, since a backdrop is a mouse shortcut
for a Cancel control that is already in the tab order, and making it a button
plants a nameless full-screen tab stop around its own contents.
Alongside those: `.sort()` calls that now say the order they always had, `{n &&
<X/>}` guards that rendered a literal "0" for a zero-byte file and for a place at
latitude 0, an optimistic budget write that stored a boolean where the contract
and both transports use 0/1, and the Number statics.
sonar-project.properties documents the six that were deliberately left: two wiki
patterns with no overlapping quantifier pair whose "fix" would stop parsing
`[see [1]](url)`, four tar header fields bounded at 12-155 bytes by the format,
two djb2 hashes where the 32-bit wrap is the algorithm, two `| 0` that are also
coercing undefined to 0 on a path where the API sends no user_id, and the MCP
session maps Sonar reads as empty because they are filled from another module.
|
||
|
|
4ba407aead |
feat(plugins): install an older version of a plugin (#2035)
Adds a version picker to the plugin admin panel, on desktop and mobile: every published version with its own server-computed compatibility verdict, so the client never re-derives semver range logic. A deliberate non-latest pick sets an update hold on the row, which keeps the update banner and "Update all" from immediately nagging the rollback away. The hold releases when an install lands back on the newest compatible version by any path, or when the admin resumes updates explicitly. An unresolvable registry never sets one: refusing to pin on missing information beats silently muting future updates. |
||
|
|
2c2d008dc2 |
fix: clear every open Sonar security issue, and half the reliability ones (#2037)
* fix(security): mint idempotency keys properly, and resolve the extractor by path Three findings out of a sweep of every open Sonar security issue. The other 49 turned out to be false positives and are documented in sonar-project.properties rather than changed — each entry scoped to a file, never a whole rule, so a future Math.random() on an auth path still fails. The idempotency key is the one that can lose data. Both producers fell back to Math.random when crypto.randomUUID was missing, and randomUUID needs a secure context — TREK's own quickstart is http://localhost:3000 and LAN installs run on http://192.168.x.x, so on the deployments the README documents the fallback is what actually runs. The axios one was `Math.random().toString(36).slice(2)`, which is not length-stable: 0.5 yields a single character and 0 yields the empty string, which the interceptor reads as "no key" and skips deduplication for entirely. Two writes sharing a key collapse into one — the second gets the first one's cached response and the edit is gone with no error anywhere. Both now come from utils/randomId, which has the getRandomValues rung that works in insecure contexts too. The extractor probe stored the bare binary name, so every extraction re-resolved it through whatever PATH held at that moment. The official image sets KITINERARY_EXTRACTOR_PATH and never reaches that branch, but a bare-metal install without it ran an unqualified name from a PATH it does not control. It now walks the search path itself and keeps the absolute path of the file it verified; execFileSync also drops the /bin/sh hop the old probe needed. PATH comes through app-config because process.env is walled off in src/. * fix: four tests and sorts that were not doing what they claimed Sonar reported 43 high-impact reliability issues. Reading all of them, four are real; the rest are cosmetic or would be made worse by the suggested change. budget.test.ts hand-built `paid: true`, but both emitters send `paid: body.paid ? 1 : 0` and the handler stores the value verbatim — the test asserted a payload the server never sends. The line below it already used a number. admin.controller.test.ts had a `.resolves` assertion with no await. The promise was dropped, so that line could not fail the test. The two to-do category lists sorted with a bare .sort(), which is byte order: every accented name files behind the whole ASCII range, so in a German trip "Übernachtung" lands after "Zelt". TREK ships 23 locales, so this is real data. One side effect worth knowing: katColor picks a category's dot colour by its index, so re-sorting can move which category shows which colour — it already moves whenever a category is added, and the colour carries no meaning. Deliberately not changed: the two `| 0` truncations (Math.trunc is not equivalent — `| 0` wraps at 32 bits and these carry millisecond timestamps), the two .map() callbacks whose second parameter is declared and used, and the sha1 in thumbnail.service (its own comment says a swap would orphan every existing thumbnail). * refactor: give every button a type, and prefer the Number statics Two mechanical sweeps, no behaviour change intended in either. A <button> with no type attribute is type="submit" per the HTML spec. Outside a form that does nothing today, but it turns into a surprise the moment the markup is wrapped in one, which is what Sonar's S9011 is warning about. 870 buttons get an explicit type="button". Two things were left alone on purpose: buttons that take a {...spread}, because the spread may carry a type and the merge order decides which wins, and anything inside a <form> — there the answer is a judgement call, and checking all eleven form files found every real submit button already carrying type="submit". The Number sweep covers only the three globals that are exactly equivalent to their Number twin: parseInt, parseFloat and NaN. isNaN is NOT in it — the global coerces its argument, so isNaN('abc') is true where Number.isNaN('abc') is false, and renaming it would change what the code does. It reaches server and plugin-sdk as well as the client; both copies of egress-policy.ts got the identical edit, so the mirror the SDK's parity test guards stays intact. |
||
|
|
dc74d59b11 |
fix(plugins): stop three ways a hostile archive or child could stall the host
All three sit in code whose whole job is to survive a plugin that misbehaves. readTarGz trusted the tar size field, which parseInt reads with a leading minus. At -512 the cursor advance cancels out and below it goes backwards, so the same header is read forever while every pass appends a member — a 70-byte upload pins the main thread and exhausts the heap, synchronously, taking HTTP, the socket and the database with it. The entry budgets never get a say: they run on the list readTarGz has to finish building first. Sideload is the reachable path. The supervisor handed onMessage's promise to an EventEmitter listener, which discards it. A rejection there is an unhandledRejection, the host has no net for it, and Node ends the process — the supervisor killed by the plugin it contains. dispatch() read `'_inv' in req.params` before handle()'s try/catch, so params that are not an object threw a TypeError into exactly that discarded promise. |
||
|
|
bc59289f73 |
fix(plugins): keep a plugin id from pointing outside its own directory
`POST /api/admin/plugins/:id/uninstall` walks its route parameter into pluginCodeDir() and hands the result to a recursive remove. Express decodes `%2F` after routing, so `..%2F..%2Fuploads` matches the route and reaches the helper as `../../uploads` — path.join then lands in the uploads tree, and `deleteData` gets the data root the same way. Verified against the express version in the lockfile. pluginCodeDir and pluginDataDir now reject an id that could leave its parent. Narrower than the manifest's ID_RE on purpose: that rule wants 3-40 lowercase characters and belongs at install time, while this one only has to make the id unable to steer a path, so it stays permissive about length and case. Every other filesystem sink in the backend already has its guard — storage keys, extraction, the wiki, backup filenames. This one did not. |
||
|
|
40957e909b |
refactor: drop two returns that could only ever say one thing
Both functions had two exits handing back the identical value, which reads as a decision and is none — Sonar's S3516, and the last two Blockers on the backend. colorizeImportedTracks returned the very object it was given, coloured in place. Saying `void` and letting the two importers return their own result is what the function was doing all along. PLACES-SVC-005 checked the pass-through and now checks the thing worth checking: an empty import writes no colours. acceptInvite bailed out early on a refusal only to return the same result the line below returns. The notification is what the early exit was really skipping, so it is the notification that gets the condition. |
||
|
|
fb76074e7c |
feat(mailer): say so when SMTP certificate verification is off
SMTP_SKIP_TLS_VERIFY (and the matching admin setting) turns off certificate checking for outgoing mail. That escape hatch stays — an install behind an internal relay with a self-signed certificate depends on it — but until now it downgraded the connection without a word, and a transport is built per send, so whoever flipped it had nothing to go on afterwards. It now warns once per process, naming the host it applies to. Once, not per mail: a line on every notification is a line nobody reads. |
||
|
|
e3c524dd02 |
test: give every test case an assertion of its own
SonarQube's S2699 flagged 196 test cases as assertion-less. Almost all of them end on a bare `await screen.findByText(...)`, which does throw when the text never shows up — the test is real, the analyser just cannot see it. Spelling the check out keeps the behaviour identical and makes it readable. Writing them out turned three tests red, each for a good reason: - FE-COMP-NOTES-008 read the heading out of the loading panel, which renders its own copy and swaps the whole node out once the fetch settles. It now waits for the loaded state. - FE-COMP-PHOTOPROVIDERS-014 matched /connected/i, which also matches the "Not connected" the badge starts as — the test passed whether or not the button did anything. Exact text now. - FE-FILES-002 compared an empty list against the empty list it already was. It seeds a file first, so the 500 has something to leave alone. JOURNEY-SVC-066 asserted nothing at all behind a "should not throw" comment; it now says that out loud and checks no journey entry is left orphaned. |
||
|
|
decc0e1a12 |
fix(plugins): boot enabled plugins after the rpc-registry scan, not before
PluginRuntimeService booted enabled plugins from onModuleInit, but Nest fires
same-module onModuleInit hooks in providers-array order, where the runtime
precedes PluginRpcRegistryService — so every boot-activated plugin's rpc host
was bound from a still-empty registry and its first RPC (typically db.migrate
in onLoad) came back PERMISSION_DENIED on every cold boot, until an admin
deactivated and reactivated it by hand. Move the boot reconcile to
onApplicationBootstrap, which is guaranteed to run after every module's
onModuleInit, registry scan included.
Also fix the denial message, which interpolated the plugin id where the
permission belonged ('db.migrate requires a permission "featured-guides" was
not granted'); it now names both the missing permission and the plugin.
|
||
|
|
d110b3bb6d |
Fix/plugin audit findings (#2031)
* fix(plugins): enforce manifest apiVersion at install and activation * test(plugins): exercise the absent-apiVersion default * feat(plugins): generate the core-event catalog into host-facts * docs(plugins): explain the packing.listBags write-permission binding * test(plugins): cover apiVersion upload refusal; tidy docblocks and generator header * fix(plugin-sdk): validate settings options and oauth like the host * fix(plugin-sdk): model the full manifest shape and stop dropping fields * fix(plugin-sdk): drop non-string routeProfiles icons like the host * docs(plugin-sdk): document host limits, oauth contract, and ignored asUserId * feat(plugin-sdk): enforce daily budgets and meta quotas in the mock host * feat(plugin-sdk): expose the generated core-event catalog * feat(plugin-sdk): scaffold trek range >=3.4.0 <5.0.0 ahead of TREK 4.0 * feat(plugin-sdk): scaffold the oauth broker settings for oauth:client plugins * chore(plugin-sdk): fix stale mirror docs and the colliding permissions export * docs(plugin-sdk): add the rate-limit string; restore trek docblock; match host meta sizing * feat(plugin-sdk): raise the scaffold trek floor to 4.0.0 * docs(wiki): bring the plugin pages in line with the current runtime * docs: bring the workspace CLAUDE.md guides up to date and make them committable * feat(plugin-sdk): roll back failed publishes, add unrelease, sync the fork, retro-sign updates * docs(wiki): document the publish rollback, unrelease, fork sync, and retro-signing * chore(plugin-sdk): 1.6.0 |
||
|
|
4eee08e45c |
test(middleware): cover the branches the forced-HTTPS change added
The redirect grew three paths nothing exercised: a malformed APP_URL falling back to the request host, a request the proxy already terminated TLS for, and the health probe, which talks plain HTTP on the loopback and must not be sent to a hostname it cannot resolve. The per-domain coverage ratchet caught the gap - src/middleware sat at 93.44% against its 94% pin. |
||
|
|
38f3cb3101 |
fix(server): the second pass over this branch's own fixes
An adversarial review of what had already landed here turned up three that
were wrong, and they are the reason for this commit.
The CSP change removed 'unsafe-eval', which is not leftover: heic-to's
libheif build initialises embind through new Function(), and without it every
iPhone .heic upload fails in the browser. 'wasm-unsafe-eval' alone was tried
once already and was not enough (
|
||
|
|
3e0bfbbb29 |
fix(client): give the desktop halves the same fixes their mobile twins got
Several of the fixes in this branch landed on a mobile screen while the desktop component with the identical bug sat in another package and was left alone, which leaves the two disagreeing about the same handler. So: the appearance tab flushes its debounced write on unmount, the journey share copy guards navigator.clipboard (undefined on a plain-HTTP self-hosted install), the admin collab toggle rolls back from a scoped snapshot instead of a stale one, the reservation modal parses metadata through the guarded helper rather than a bare JSON.parse that takes the route down, and the notification toggle surfaces its failures and stops racing concurrent toggles. The Journey components reached the memories provider-status endpoint with a raw fetch(), past every apiClient interceptor; they go through a small memoriesApi now. And the server half of the OSM menu_url finding: the client renders it through safeHttpUrl, so place-enrichment should not be handing a javascript: value out in the first place. The share section hides itself when the server says the link is not this user's to manage, rather than offering a create button that 403s. |
||
|
|
eadb3703bc |
fix(shared,tests): bound the book contract and close the gaps that hid all this
Book page setup and element geometry were unbounded numbers, so a shared book could be saved with zero, negative or astronomical dimensions and every consumer downstream - the canvas, the PDF export - had to cope. Day-note caps had drifted from the limits their own doc comment states, and icon had none. On the test side: the path filter meant a PR touching only the root lockfile ran no jobs at all, which is exactly the change class most likely to break a native binding; the canAccessTrip mock is hand-copied into 72 files and had already drifted from the real query; the path-traversal test asserted only status !== 200, so a 500 from a broken guard passed it; and one unit test shelled out to a real kitinerary binary if the host happened to have one. |
||
|
|
a7aaa29c16 |
fix(db): stop losing secrets and notes to the boot and migration paths
An unreadable or empty encryption-key file was treated the same as a missing one: regenerate and overwrite. That is the one case where regenerating is the worst possible move - every secret already at rest becomes undecryptable, permanently. Both now refuse to start and say which file to fix. The key-rotation script advertised re-encrypting all stored secrets while its hardcoded column lists had drifted: the AirTrail key, the per-user LLM key and the plugin OAuth tokens were never migrated, so they silently stopped decrypting after a rotation. The parity test now pins the full set against what the code actually encrypts, rather than two tables of it. The schema_version bump ran outside the migration's transaction, so a crash in between left the schema advanced and the version stale - the next boot replayed a non-idempotent step and exited, turning one crash into a boot loop. And the TICKETJSON receipt migration matched with LIKE, which SQLite treats case-insensitively, so a note somebody had typed starting 'ticketjson:' was cut up and its text dropped. Also: APP_VERSION was interpolated into migration SQL instead of bound, the capture-metadata backfill passed (photoId, userId) to a (userId, photoId) signature and its mock keyed on the wrong argument, TRUST_PROXY=0 was accepted and silently coerced to 1, demo-reset hardcoded the db path instead of honouring TREK_DB_FILE, and a demo instance with no DEMO_ADMIN_PASS set now says out loud that its admin password is the published default. |
||
|
|
8e5fdaa877 |
fix(server): tighten the guard, audit and token paths
Permissions cached an empty map before it had loaded, so a DB error during the load cached permissive defaults and every later check answered from that cache - fail-open, and it never expired. The rate limiter grew its per-IP map forever; the GC the legacy version had was dropped in the fold. Idempotency did a check-then-insert, so two concurrent duplicates both ran the handler. Audit rows recorded whatever X-Forwarded-For said, which the client writes, and the request-body debug logger redacted by exact key match - it missed refresh tokens, api keys and PKCE verifiers. FORCE_HTTPS built its redirect from the client's Host header. CSP asked for unsafe-eval on top of the wasm-unsafe-eval it actually needs. The OAuth deny path redirected to an unvalidated URI, ws-token minting had no rate limit at all, and PKCE was skipped when code_verifier was absent rather than refused. An MCP session resumed with a token whose scopes had since been reduced kept the capability set it was created with, and the session cap was checked before the async registration that increments it. Plugin frame containment was lexical, so a symlink out of the directory was never noticed, and instance config accepted keys the manifest never declared. |
||
|
|
4bfb277423 |
fix(journey,files): close the cross-trip and role holes on the sharing surface
Files: two :id handlers took the file id from the URL and never resolved it against :tripId. Membership of any trip was enough to read another trip's file links (with the joined reservation titles) or to delete one. Both now resolve the file the way every sibling handler already does. Journey: a contributor with role 'viewer' - blocked from every other write in the domain - could overwrite or delete the Studio book, and could read the share token, which is an anonymous credential that outlives their contributor status. The book writes take canEdit now and the token read takes the owner check its own create and delete already use. Also: the share-link MCP tool reset every flag to permissive defaults when it was called to refresh a link, so a journey deliberately shared without its gallery or map quietly started publishing both; setShareLink dropped newest_first entirely; the public routes ignored the Journey addon switch; and photo uploads committed to storage before the access check, leaking the objects on the 403 path. Trips: DELETE answered 403 to someone with no access at all, which turns sequential ids into an existence oracle. A member who simply lacks trip_delete still gets 403. |
||
|
|
515b832fa3 |
fix(collab): stop serving the text of deleted chat messages
A deleted message keeps its row so the client can draw the placeholder off the flag, but listMessages still returned the original text - in the list itself and in the reply preview joined onto whatever quoted it. Both REST and the MCP resource read through that one method, so blanking it there covers every surface. Also cap the link-preview read, and delete the spooled upload when addNoteFile refuses the request instead of leaving it on disk. |
||
|
|
97ebe23d3b |
fix(server): bound every outbound fetch
The rule is a timeout, a size cap and boundary validation on anything that leaves the process, and several call sites had only the timeout. A link preview, a maps URL resolve, a wiki fallback and a holiday lookup all read the whole body into memory, so a caller who picks the URL picks how much memory the server spends - a few concurrent multi-hundred-megabyte responses is the whole heap. Two of them also fetched more than their name suggests. resolve-url fell through to a full page GET for any host once it found no coordinates, which makes the server a general-purpose outbound proxy wearing its own IP. The weather URLs took lat/lng straight from the caller into the query string. readCapped/exceedsDeclaredLength collect the pattern transit.service.ts and the plugin installer had each grown separately, so there is one of it now. Callers that only scan the head of a document (og: tags, a coordinate regex) keep the truncated bytes and find fewer fields; callers that must have the whole thing reject it. |
||
|
|
b163c1e05d |
fix(server): hold DELETE bodies to the same contract as every other write
The body-contract boot gate only looked at POST/PUT/PATCH, so a DELETE that reads a body slipped past it. Two do; one was already typed, the other read photo_id straight off an untyped Record. Give it a contract from shared like everything else, then add DELETE to the gate so the next one cannot repeat it. |
||
|
|
3f8a7a1ea9 |
fix(feeds): keep subscribable calendars working under default-deny
The ICS feed routes are documented as public - the secret token in the URL is the credential - but nothing in the code said so, and the guard this release makes global is default-deny. Every calendar client subscribed to /api/feed/trip/<token>.ics would have started getting 401 on upgrade, and a calendar app that stops fetching just goes quiet. The e2e suite asserted anonymous access the whole time. It booted [DatabaseModule, RealtimeModule, FeedsModule], and the APP_GUARDs live in AppModule, so it was asserting against an app that had no opinion on anonymity. It boots the real guard stack now, and fails without the fix. The boot ratchet could not have caught this either: it only looked at routes carrying @Public(), and GlobalAuthGuard stands down for ANY declared chain - so a chain of guards that refuse a caller but never resolve one answers strangers, invisibly. Split the inventory in two and write that half down as well. It turns up two routes nobody had listed: the passkey login ceremony, which genuinely runs before a session exists. |
||
|
|
d91eda690a |
fix(reservations): keep every id in a booking inside its own trip
reservation_edit on the trip in the URL says the caller may write here. It says nothing about the ids they put in the body, and the write contracts are open records, so day_id, place_id, assignment_id and accommodation_id all arrived unchecked. A booking created in one trip could carry another trip's accommodation_id, and deleting it ran the cascade against that foreign row. The MCP tools have refused foreign ids since they were written. Put the same check where every writer reaches it, and scope the two day_accommodations statements to the trip so a row stored before this still cannot be followed out of it. |
||
|
|
e9090fa33c |
fix(notices): confine the 4.0.0 release modal to the 4.0.x line
Without an upper bound it would greet somebody who jumped 3.x straight to 4.2 with a summary of a release they skipped, and it would run alongside the next release's own notice. maxVersion is exclusive, so 4.1.0 covers every 4.0.x patch. Pinned in the registry test, together with the handover from the thank-you notice: at no version may both be active. |
||
|
|
76c8c349b1 |
Fold nest mcp (#2017)
* refactor(server): fold @trek/nest-mcp into server/src/nest-mcp * docs(server): point nest-mcp references at server/src/nest-mcp * remove tracked md file |
||
|
|
305b113d5f |
Feat/storage driver (#1949)
* feat(storage): storage key validation, driver contract suite, LocalDriver * feat(storage): MirrorDriver composite backend * feat(server): add TREK_UPLOADS_DIR env var for the uploads root * feat(storage): settings-backed registry, StorageService facade, module wiring * test(storage): pin src/nest/storage coverage thresholds * feat(storage): multer upload options factory * refactor(files): spool trip-file uploads through storage * refactor(collab): spool note attachments through storage; normalize files/ prefix * refactor(journey): spool journey media uploads through storage * refactor(trips): spool trip cover upload through storage * refactor(places): spool place-image and collection uploads through storage * refactor(auth): spool avatar upload through storage * fix(server): consolidate allowed_file_types onto AllowedFileTypesService * fix(storage): default defParamCharset to utf8 in the upload factory * test(db): re-point the trailing-migration guard at the trip_files prefix strip * fix(storage): age-gate the boot spool sweep * test(files): key FILE-P02 residue checks on upload size * test(storage): raise src/nest/storage coverage pin * refactor(platform): serve /uploads routes through storage * refactor(files): serve trip-file downloads through storage * refactor(memories): serve photo bytes through storage * refactor(maps): stream place-photo bytes through storage * refactor(share): stream shared place-photo bytes through storage * refactor(journey): serve public journey assets through storage * fix(share): answer place-photo cache miss with 204 like the maps proxy * test(server): close serving-swap gaps surfaced by the full tier * test(storage): raise src/nest/storage branches pin * refactor(place-photos): route place-photo cache through storage * refactor(places): reclaim place images and collection covers through storage * refactor(trips): trip covers through storage * refactor(auth): avatar reclaim through storage * refactor(collab): note-file deletion through storage * refactor(files): trash deletion through storage * refactor(memories): trek photo cache internals through storage * refactor(memories): thumbnail service category-addressed * refactor(memories): immich upload reads through storage; drop uploads-root * refactor(journey): photo deletion through storage * fix(memories): reclaim orphaned trek photo cache objects in the ttl sweep * feat(journey): journey-thumbs orphan sweep and delete-site derived-artifact reclaim * test(server): close slice-4 gaps surfaced by the full tier * test(coverage): raise slice-4 domain pins * refactor(backup): list, existence and delete through storage * refactor(backup): download through storage sendToResponse * refactor(backup): create backup through per-category storage enumeration * refactor(backup): restore and upload-restore through storage * refactor(backup): auto-backup retention through storage * refactor(server): boot dir-creation to driver init * test(server): close slice-5 gaps surfaced by the coverage tier * test(coverage): raise slice-5 backup pin * test(server): resolve async backupFileExists mocks for the tests typecheck * test(server): pin ranged-read stat and zero-byte roundtrip in the storage driver contract * feat(server): TREK_S3_* env wiring — schema, s3 namespace, all-or-nothing boot rule * feat(server): S3 driver core — aws-lite client seam, stat/delete, driver-owned deadlines * refactor(server): type the aws-lite S3 client via s3-types reference * feat(server): S3 driver getStream and list — ranged full-object stat, iterator pagination * feat(server): S3 driver put — bounded-peek routing, Upload inactivity deadline * fix(server): S3 driver Upload monitoring — Transform interposition, stream cleanup * feat(server): env-declared s3-main storage backend; settings-declared s3 rejected * test(server): env-gated S3 driver contract suite against MinIO * fix(server): S3 driver zero-byte put — work around aws4 duplicate Content-Length signing defect * fix(server): scope the zero-byte put workaround to buffered bodies; bind test MinIO to localhost * ci(server): MinIO-backed S3 driver contract job * ci(server): align s3-contract shim comment and MinIO bind with references * docs: S3 storage backend configuration and off-box backup mirroring * docs: restore README formatting, keep S3 additions only * ci(server): make live-MinIO contract runs manual-only (AIStor licensing) MinIO's community line is discontinued entirely; the maintained successor, AIStor, requires a license key that can't safely live in a shared CI runner. The s3-contract CI job is removed; live-provider contract runs against MinIO/AIStor are now a manual, license-gated procedure documented in docker-compose.minio-test.yml's header. * chore(server): final-review polish — stale CI refs, dockerignore key, boundary pins, zero-byte tempfile contract case * expose admin ui * feat(shared): storage backend type registry, wire schemas, category relocation * feat(server): accept settings-declared s3 backends — shared-schema options, decrypt-at-build, registry preview() * feat(server): remove TREK_S3_*/TREK_UPLOADS_DIR — tombstone boot rule, built-in override as the relocation path * feat(server): deriveSecurity.encryptionKeySet and storage secret plumbing (gate, encrypt, unmask, mask, redact) * chore(server): drop storage-secrets import that only Task 8's probe helper will use * feat(server): seed-once storage-config.json boot import — fail closed on any invalid seed * feat(server): registry snapshot() — effective storage config with provenance * feat(server): api/admin/storage GET/PUT — masked secrets, preview-gated saves, wired reload, audits * feat(server): api/admin/storage/test — per-target ephemeral probes, mirror targets probed individually * test(server): e2e coverage for api/admin/storage — guards, managed refusal, envelope, probes * test(server): give e2e harness DBs the app_settings table the storage registry reads at boot * test(coverage): raise pins after the storage admin-config slice * fix(server): final-review polish — encryption-gate wording, all-field sentinel guard, README/e2e notes * test(server): storage slice-1 review follow-ups — mirror s3-replica probe decrypt, all-encrypted seed without key, test-route 401 * feat(i18n): storage admin domain + tab label in every locale * feat(client): adminApi storage endpoints — getStorage, updateStorage, testStorageBackend * feat(client): storage admin draft-document helpers + relative-time util * feat(client): schema-driven storage BackendForm — fields by kind, mask echo, encryption banner * feat(client): admin storage panel — backends list, category map, health strip, one-PUT save * feat(client): storage tab on the admin page * feat(client): storage admin panel and section on the phone shell * fix(client): block renaming a storage backend onto an existing name * feat(server): allow mirrors on any storage category — lift the backups-only rule * feat(i18n): replicas-on-primary mirror strings in every locale * feat(client): mirror fold/synthesize model helpers for replicas-on-primary * feat(client): BackendForm mirror-targets composer; raw mirror type hidden * feat(client): desktop storage panel folds mirrors into their primaries * feat(client): phone storage panel folds mirrors into their primaries * fix(client): strip removed backends from remaining mirror replicas — no phantom refs * feat(storage): retire photos as a configurable category — served-only via the ServedCategory seam * feat(i18n): descriptive storage category names and descriptions in every locale * feat(client): descriptive category names, descriptions and id badges in the storage panel * docs(storage): fix stale nine-category comments after the eight-category shrink * docs: storage configuration via the admin UI and seed file — README + compose * docs(wiki): Admin Storage page and storage cross-links * docs(server): storage domain blueprint entry; TREK_PLACE_PHOTO_DIR tombstone clarifier * refactor(server): drop the storage env-var tombstones — the variables never shipped * docs: storage config is simply new in v4 — drop the removed-variables story * feat(shared): backfill-status and usage wire schemas on the storage admin state * feat(server): MirrorDriver.backfill — prefix-scoped, size-compared, cancellable replica catch-up * feat(server): one-at-a-time storage backfill jobs + the storage events bridge * feat(server): storage usage scan — nightly cron, on-demand service, persisted with computedAt * feat(server): replica_failure admin notifications — events bridge, hourly debounce, channel registration * feat(i18n): replica-failure notification text in every locale * feat(server): storage backfill and stats-refresh admin endpoints * feat(client): adminApi backfill and stats-refresh endpoints * feat(i18n): storage sync and usage strings in every locale * feat(client): desktop storage panel — sync now, usage display, backfill polling * feat(client): phone storage panel — sync now, usage display, backfill polling * fix(client): per-category usage line on the phone storage panel * docs: sync now, usage stats and replica-failure notifications * test(server): admin scope now includes replica_failure, not just version_available Commit |
||
|
|
6a5f067663 |
fix(atlas): give Great Britain its counties and boroughs back
Zooming into England showed nothing below "England" itself, so boroughs marked as visited had no polygon to light up. It stopped working in 3.1.0, when the runtime Natural Earth fetch was replaced by the bundled geoBoundaries assets: Natural Earth carried 232 GB regions including the London boroughs, and geoBoundaries calls the four constituent countries ADM1. The builder asks every country for ADM1, so GB shipped as four features. GB now ships at ADM2 — 216 counties and unitary authorities, which is the granularity every other country gets from its ADM1 and the granularity that was there before. That is a deliberate one-country list: ADM2 everywhere would multiply the bundle for no gain, since ADM1 already is the level people think in for most countries. Two things had to be said to data that already exists. Regions marked before 3.1.0 hold the old Natural Earth codes, and are reconciled by name within the country, the way the 3.1.1 step already does for everywhere else; a row that still cannot be matched is left alone rather than destroyed. And place_regions is full of GB-ENG rows that were correct under the old bundle — nothing re-derives a row that is already cached, so without clearing them every English place would keep reporting "England" forever. The bundle guard that would have caught this in 3.1.0 is now in place: four features for a whole country is not a region layer. Closes #1974 |
||
|
|
0ce82b811b |
fix(atlas): count only the flights a person is actually on
4.0 lets a booking name the people it is for (#1517), and the travel figures never read that. They scoped reservations by trip membership, so on a shared trip everyone was credited with everyone's flights: two people meeting in New York, one from Rome and one from Mexico City, each got both departures, and each had "visited" the other's country. The rule now asks whether the reservation belongs to the person asking, in one fragment used by all three queries that derive figures from reservations — flown distance, the passport card's countries, and Atlas's own endpoint scan — so there is one answer to that question rather than three. Its second half is the important one: a booking with nobody named on it counts for everyone on the trip. Every reservation made before 4.0 looks exactly like that, and a plain join would have taken the flown distance of every upgrading install to zero. With no assignments anywhere the numbers are identical to before. Where someone HAS assigned travelers, their figures will go down after this, because they were counting journeys they did not make. That is the point, but it is worth a release note. Places are untouched: they have no per-traveler concept, and this is only about numbers derived from bookings. Closes #1966 |
||
|
|
9e13791239 |
fix(booking-import): geocode transport endpoints on the path clients use
An imported train, bus or rental car showed its pick-up and drop-off in the review step and then had them gone from the saved booking: no pins, no route, no error. reservation_endpoints.lat and lng are NOT NULL, so saveEndpoints drops an endpoint without coordinates, and kitinerary and the LLM name stations, stops, terminals and rental desks without geo-locating them. Only airports arrive with coordinates, from the mapper's own table. The lookup that was meant to fill them in existed — in confirm(), which no client calls. Both the desktop planner and mobile go preview, review form, ordinary save, so it never ran. It runs in preview() now, through a helper confirm() shares, on the throttled background lane whose own docstring names booking-import as its intended caller. The query ladder matches the venue lookup beside it: the endpoint plus the booking's location first, then the address, then the bare name — name-only is exactly what fails for a desk label like "Curbside Pickup Counter 7", so it goes last. Answers are cached per preview, since a multi-leg train repeats its stations and the lane allows about one request a second. An endpoint that still cannot be placed stays on the item rather than being filtered, so the review form can show and edit it, and the preview says which name it could not locate. That warning needed the widget to change too: it rendered warnings only when a job found nothing at all, which is precisely the case this is not. Reported with the full chain, file by file. Closes #1969 |
||
|
|
a5e468829c |
fix(llm-parse): stop discarding an extraction Anthropic sent as a string
With the Anthropic provider, an import sometimes reported that no
reservations were found even though the document held one and the model had
extracted it. Nothing was logged, because from the code's point of view
nothing failed: the value simply was not an array, and the check that only
accepted arrays returned an empty list. What reached the user was
indistinguishable from a document with no booking in it.
The tool declares `reservations` as an array and the call forces the tool, so
it usually is one. Sometimes the model serialises the same content as a
JSON-encoded string instead — either a stringified array, or stringified and
wrapped in `{ reservations: ... }` again. Because that is a serialisation
choice made per call, the same file imported fine on one attempt and came
back empty on the next.
Normalising is now one function next to the lenient JSON parser, and the
OpenAI-compatible client uses it too — it had grown its own version of the
object-unwrap half, and two copies of this rule would drift.
It unwraps a string once and no further. A value that is still not a list
after that is not a list, and reaching deeper would start turning prose into
bookings.
Reported with the exact file and lines, and with both observed shapes.
Closes #1968
|
||
|
|
9522e4ce4a |
fix(budget): keep an uneven split from corrupting the expense total
163.21 across two people is 81.61 and 81.60. Both are exact to the cent — the client splits properly — but the server discarded the total it was sent and re-derived it by adding those two as doubles, which lands on 163.20999999999998. That is the number that went into the row. It looked like a display quirk because everything routed through Intl came out right; only the places that print a number as it stands showed it. The stored value was wrong all along, which is why reopening the expense showed it back, and why the price stamped onto a linked booking carried it too. The domain already had this rule and followed it everywhere else — the settlement maths converts to whole cents before it adds anything. The payer sum was the one arithmetic still done in euros, in both the create and the update path, so it now goes through the same rule. The client side is what makes rows that are already stored wrong stop looking wrong: the expense form and the mobile cost sheet seed their total from a cent-clean value, and the three panels that print a booking's price verbatim tidy it when it parses as a number. Free text an import wrote there passes through untouched rather than becoming NaN. Closes #1964 |
||
|
|
8f171453d3 |
fix(packing): keep a private item off the other members' screens over MCP
A restricted packing item, changed through an MCP tool, went to every socket in the trip room. Ticking something off your own list through an assistant pushed that row to every other member. The REST and RPC surfaces have scoped these events since #858 — both go through emitToViewers, which delivers to the owner and the recipients and nobody else. The MCP tools called safeBroadcast, which took three arguments and had no way to say who may see the thing that changed. So safeBroadcast learned that fourth argument, and the packing tools pass viewersOf(item), which is the same answer the REST route asks for. A shared item still answers null and still goes to the room in a three-argument call, so nothing about it changes — including what the hundred-odd broadcast mocks in the suite record. The client stops trusting the wire on this too. It used to `put` whatever arrived, and the offline read hands back every cached row for the trip with no privacy filter, so a single stray event lodged another member's item in that browser permanently. It now drops a restricted item that is neither mine nor shared with me — deleting rather than skipping, so a list that was already leaked into heals itself the next time an event about that item comes past. Closes #1976 |
||
|
|
eae8480728 |
feat(studio): follow-ups from using Studio on a real journey (#1984)
* feat(studio): widen the document, and stop a save from losing it The schema gains what the elements coming after it need: an icon element, a badge that can be restyled rather than only placed, a date and a pair of coordinates on a text binding, and the map's route, pin and road settings. Widening it turned up two ways a book could fail to come back. The mood badge writes its code into the document, and the schema said that code was exactly two characters, from a first draft where it was a country. The real codes are words, so every save was rejected and the read that followed parsed to an empty document: seventeen spreads in, nothing out. Widened to what the badge actually writes, and normalizeBookDocument now drops the elements it cannot read rather than the whole book, so one bad element costs a picture frame instead of a year of work. The other was the body limit. The parser allowed 100kb for everything, and a book with a routed map measures past that, so the save came back 413 and the editor sat on "not saved" with no way forward. Book writes get their own parser at 8mb; everything else keeps the limit it had. * feat(studio): a date, a pair of coordinates and an icon library Three things that had to be drawn by hand before. A journal entry knows when it happened and where, and neither could be put on a page: you read the date off the entry and typed it into a text box, where it then stayed while the entry moved. Both are chips now, next to the title and the story, and both follow their entry the way the others do. The formatting lives in one place so the chip, the template and the resolver cannot drift by a character — coordinates in degrees or decimals, the date in the reader's language. The icons are the whole Lucide set, searchable, placed as a real element with its own colour and stroke rather than as a character in a font. The stroke is deliberately not called strokeWidth: three separate places rescale a field of that name by page width, and an icon whose line thins as the book grows is not what anyone asked for. And the badges finally have their settings. A mood or weather badge could be placed and then only moved: the icon's colour was fixed, the icon could not be turned off, and there was no way to keep the icon and drop the words. All three are switches now, with the colour following the badge unless you say otherwise. * fix(studio): let a placed entry field follow its journal entry again The inspector says a bound field follows the journal entry and that editing it here breaks the link. The first half was not true: nothing ever re-read the entry, so renaming a title in Journey left the old words on the page for good, and the same went for every other bound field. The resolver runs where the journey is loaded and re-reads the fields that still point at an entry. What it will not do is as important as what it does. A field somebody has typed over stays typed over, an entry that is gone leaves its element alone rather than blanking it, and an entry that answers with nothing never wipes what is already on the page. Formatted fields — the date, the coordinates — are only re-read when the format itself changed, which is what keeps this from quietly rewriting books that already exist: an entry whose date was placed as a heading and then set in capitals keeps its capitals. Checked against the real database before it shipped, where the first version overwrote three of them. * feat(studio): start a new book empty, with six layouts to choose from A new journey opened onto a book that had already been laid out, which is the wrong first move: it decides the shape of somebody's book before they have seen it, and undoing it means deleting pages one at a time. A new book starts empty now, and the auto layout runs when it is asked to. The layouts themselves went from two to six, so choosing one is a real choice: what was there covered a photo-heavy spread and a text-heavy one and nothing in between. * fix(studio): hyphenate the export the way the editor does, and set the bleed Two things the press sees and the editor did not. A paragraph broke in different places in the PDF than on screen. The print document is written from scratch and never carried a language, so the browser hyphenated the editor's German and the export's nothing-in- particular. It takes the page's own language now. The wait for the sheet to be ready also covers the images inside SVG, with a ceiling, so a map that is slow to draw does not print half-finished. And the bleed and the safe margin are typeable. Both were baked into every page preset at what a photo-book vendor usually asks for, 3mm and 5mm, with no control anywhere. That is fine until a printer asks for 5 and 10, at which point the book cannot be made here at all: the export carries the wrong bleed and the crop marks sit in the wrong place, which nobody finds until the guillotine has run. * feat(studio): rebuild the inspector The panel on the right had grown by accretion: every element type had added its own controls in whatever shape suited it that day, so the same idea — a colour, a size, a toggle — looked different depending on what you had selected, and long panels were a wall of unlabelled fields. It is one set of controls now, used by every element: foldable sections, a line that holds a label and its control, radio choices, checkboxes, the app's own toggle, and a number field that holds a draft so typing "500" is not read as "5", clamped and written back under the cursor. Duplicated controls were merged rather than deleted; nothing an element could do before it cannot do now. The head carries the element's identity and its actions, so what you have selected is readable without counting fields, and the sections have room to breathe: what was there was correct and unreadable. * feat(studio): a route map worth printing The map in a book was a straight grey line between grey dots on a tile that was chosen for a screen. At print size those tiles are read at roughly four times the resolution they were fetched for, which is why it looked soft next to everything around it. The imagery comes from sources that hold up on paper: NASA's shaded relief and EOX's cloudless Sentinel-2, both licensed for it, picked at a zoom that matches the printer rather than the screen. Tiles are fetched at twice the density where the source offers it, and the seam between them scales with the tile instead of sitting at a fixed fraction of a millimetre, which under editor zoom fell below one device pixel and showed as white threads. The route is drawn rather than plotted. A leg that was never recorded bows, by an amount that grows with its length and turns away from the pole, and it dashes, because an inferred line should not claim to be a track — a recorded track never bows. Both strokes are laid down in two passes, casing then core, so the line stays legible over dark sea and bright desert alike. Legs can follow roads instead, routed leg by leg with the geometry thinned before it is stored, which is the setting a road trip wants and the one a flight cannot have. Stops carry their own first photograph, cut to a circle by a stencil that exists — the first one was written into a defs block that only appears on a map clipped to a coastline, so on an ordinary map it clipped nothing and the photographs sat square on the page. A stop with no photograph of its own is numbered rather than handed one from elsewhere in the journey: a marker at one place showing a picture taken at another is a caption that lies. Whatever the frame, the whole route is inside it. The window takes the frame's aspect, so the fit and the imagery agree instead of one containing what the other covers, and a map dragged narrow no longer cuts off its end. Each linked trip can carry its own map, and a map notices a photograph added to a stop afterwards rather than having to be placed again. * i18n(journey): the new Studio strings, in all 23 locales * test(journey): pin the coverage the map work brought with it The journey domain measures higher than its floor after this round, so the floor follows it up. Only journey moved; the generator's suggestion would have taken memories' functions down a point, and a ratchet that can fall is not a ratchet. * test(studio): cover the thinning that keeps a road route saveable The one path in roadRoute nothing exercised, and the one that matters most: a router answers a long drive with thousands of points at a precision no press can resolve, and all of it goes into the document. Keeping a leg whole is what pushed a real book past the body limit. Both ends have to survive so the line still meets its stops, and the spacing has to stay even, because cutting a run out of the middle would straighten exactly the bends somebody wanted to see. |