mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
4225d883c01df4d29aaa1d56bcfdf55cd8a45340
932 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
922f37ab52 |
fix(storage): ask the hook whether a storage job is running, not a stale closure
Every push to dev has been red since the 4.0.0 bump, always on the same test: FE-MOB-MSTOR-015, where the second queued migration POSTs while the first is still running. It passes on the PR runner and fails on the slower push one. Instrumenting the queue effect logs the whole story: DEQUEUE files stateMigrations [] APPLY refresh ["running"] START RESOLVED files error null DEQUEUE journey stateMigrations [] The refresh had already landed the running migration before the single-flight lock was released, exactly as intended. What fires afterwards is the effect of a commit scheduled BEFORE the POST, so its captured admin.state is the pre-POST world, and it runs late enough that the lock is gone. The lock is a ref and is read live; the "is one already running" test was read off that dead closure. Under load the two drift apart and a second migration starts on top of a running one. So the hook answers the question instead. It keeps its world in a ref as well, written where the response is applied rather than when the render lands, and storageBusy() reads that. The queue effect still WAKES on admin.state, it just no longer DECIDES on it, and the server's one-job-at-a-time rule now lives with the hook that talks to the server rather than twice in two shells. Same shape as the dirtyRef/savingRef the hook already keeps for the same reason. An earlier attempt made the panels refresh again after startMigration, which was both redundant (startMigration has awaited its own refresh since #1949) and no fix at all: the stale read is on the next effect run, not on that promise. |
||
|
|
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. @ |
||
|
|
4d6820a4d4 |
@
fix(planner): route a layover flight per leg, and count linked places as planned Two v4.0.0 reports, both a surface answering a question with less than it had. The map built its own waypoint order. It read the BOOKING position and never expanded metadata.legs, while a multi-leg booking stores its position per leg — so a Frankfurt to Vancouver flight via Reykjavik was dropped from the list and the airport before it was joined to the airport after it in one driving run across the Atlantic. It now takes the order from getMergedItems, the same one the day plan renders, which brings the span filter and the leg expansion with it. getMergedItems in turn had never read day_plan_position, the one slot the map did read, which is how the two could disagree at all. getTransportRouteEndpoints handed back the booking outermost airports for every leg. A stop between two legs was therefore routed from the arrival airport back to the departure one — a drive from Vancouver to Frankfurt with a cafe in Reykjavik on the way. A leg now spans its own pair, when the endpoints line up one per stop with the legs; anything ambiguous keeps the role lookup. Being planned meant one thing: a day_assignments row. A hotel is linked through its stay and a venue through its booking, and neither is ever dragged onto a day, so both sat under Unplanned while the day header printed their names. The three places that asked the question — desktop sidebar, map markers, phone browser — now ask one helper that also counts a stay and a day-anchored booking. A booking that names no day still counts as unplanned, because nothing says when it happens, and the day plan renders nothing for it either. @ |
||
|
|
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. @ |
||
|
|
4784a1e5dd |
fix(quality): clear the SonarQube findings on the 4.0.0 branch (#2062)
The day-swipe move handler mixed classifying the gesture with following the finger, which put it at cognitive complexity 20. The classifying half is a pure question about a gesture, so it moves out as classifyLock: null keeps waiting for the next move, dead and h end the same way they did before. Also reads the reduce-motion flag through dataset, seeds the closest-year reduce with years[0] so it cannot throw if the empty guard above it ever loosens, and compares the in-flight preview entry against undefined instead of leaning on a promise being truthy. |
||
|
|
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 |
||
|
|
800f97b8ce |
feat(mobile): swipe the day details to step through days (#2059)
* @ feat(mobile): swipe the day details to step through days The phone shell hangs the days off a chip rail pinned to the top of the screen, which on a large phone is a thin target a thumb cannot reach one handed. The day details fill the rest of the screen and did nothing. They now take a horizontal swipe: left for the next day, right for the previous one, with the panel following the finger, rubber banding at the first and last day and springing back when the swipe falls short. The chip rail is untouched and still the accessible path; a committed swipe announces the day it reached through a live region, and the active chip scrolls itself back into view once the rail overflows. The gesture never calls preventDefault or stops propagation on a touch event and sets no touch-action, so the browser keeps sole ownership of the card scroll and of the header chip strip, and the long press reorder bridge keeps its own 10px self cancel. In edit mode the swipe also refuses to claim a press held past the bridge window. @ * @ fix(mobile): line the map controls up and widen the POI bar The compass sat on its own under the POI bar, centre top, while the locate button floated 42px above the dock on the other end of the screen. Both are one handed controls, so they now share the band just above the dock: compass bottom left, locate bottom right, off the same --bottom-nav-h the map already reads, a dock gap lower than before. That frees the row under the chip rail, so the nearby places bar takes the full width between the screen margins and its segments end up the size of everything else the thumb aims at there. Desktop keeps the floating, content width bar. @ |
||
|
|
495c5e95ec |
fix(mobile): drop stale up-next cards and open the plan on the next day (#2057)
The UP NEXT card kept pointing at this morning's first stop hours after it had started, and at day one of a trip that ended last month. A header promising what comes next, sitting above something that is over. It now stays away on a day that has passed and once today's timed plan has run out — a day whose stops carry no times at all still shows its first stop, there is nothing stale about that. The timeline card already collapses the reserved space when there is no up-next, so the layout closes cleanly. Seeding the plan day also lands on the next day still ahead when today falls into a gap in the dates, instead of dropping back to day one. A trip that is already over keeps opening on its first day. And a day that was selected before the shell mounted now counts as seeded, so clearing the selection is left alone instead of being re-seeded on the next render — which is what the guard was there for in the first place. Based on #2055. |
||
|
|
f61bab3ffd |
fix(vacay): open on the current year instead of the last one added (#2056)
The year list comes back sorted, so picking its last element meant Vacay always landed on the highest year — add 2030 to plan ahead and every visit from then on opened 2030. It now opens the leave-year period today falls into, falls back to the closest year the plan has when that period has none, and keeps the year the viewer is on when a live update reloads the list. |
||
|
|
f605b730d8 |
fix(journey): improve video thumbnails and playback layout (#2043)
* Revert "chore(deps): bump dompurify from 3.4.11 to 3.4.12 (#1681)" (#1694)
This reverts commit
|
||
|
|
5b9aaab847 |
fix(client): mint upload idempotency keys via randomId()
uploadQueue called crypto.randomUUID() bare, which is undefined on non-secure origins (plain-http localhost and LAN installs) and threw instead of uploading. Route it through randomId() like the other two idempotency-key producers so it falls back to getRandomValues, and update randomId's header comment to name all three producers. |
||
|
|
0269273d01 |
fix(budget): use Math.trunc for user colour index, complete settlement fixtures
The `| 0` truncation in colorForUserId/colorFor existed to coerce an undefined user_id to 0, but the settlement API always returns user_id — only the two BudgetPanel test fixtures omitted it. Add user_id to the fixtures, switch both helpers to Math.trunc (with a Number.isFinite guard in colorForUserId preserving the old undefined->0 behaviour), and drop nothing else: TravelerPicker only ever receives required numbers. |
||
|
|
105cb68316 |
fix(mobile): text fields no longer show a dark box inside their row
iOS paints its own background inside a text control, and under color-scheme: dark that fill is darker than the card the field sits on. The mobile fields are deliberately bg-transparent because the row supplies the surface - which is why the native fill has nothing covering it and shows through as a dark rectangle, while a desktop field hides it behind its own background. Closes #2028 |
||
|
|
c435033343 |
refactor: one endpoint-name cleaner for both map renderers
The scan that replaced the parenthetical-stripping regex went into the Leaflet overlay and its Mapbox GL counterpart as two byte-identical copies, which is what the duplication gate caught. trackColors.ts next door exists for exactly this reason - both renderers read a track's colour through one function so it cannot come out different on one map than the other - so the cleaner follows it. Behaviour is unchanged. The test brute-forces the new function against the regex it replaced over all 55,987 names up to six characters of "ab() \t", and pins the 32k-space input that used to take a second. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
52d1a0720f |
fix: say out loud what every sort was already doing
Sonar's S2871 flags a bare .sort(), which orders by UTF-16 code unit rather than by value. All 30 remaining sites turned out to sort ASCII strings — ISO dates, currency and country codes, setting keys, route names, OAuth scopes, zip entry names — where that order is already the intended one. Each says so explicitly now, with a comparator that reproduces it exactly rather than a localeCompare that would quietly reorder things. That mattered more than it sounds. Several of these feed something other than a display: useAtlas joins the sorted codes into the signature that decides whether to rebuild the region layer, daysSlice re-pins sorted dates positionally onto reordered days, and three timeline views pick a day's colour by indexOf into the sorted array. Switching any of them to a locale collator would have been a silent regression. Two smaller ones came with it. The .map(placeBtn) calls in FileManagerAssignModal pass the index as a second argument, which placeBtn declares and uses for its React key — correct already, now written out as .map((p, i) => placeBtn(p, i)). And budgetSlice's optimistic paid-toggle stored the raw boolean behind `paid as unknown as number`, while the schema declares z.number() and both the REST response and the WebSocket event carry 0/1. So the optimistic state and the confirmed state disagreed on type until a refresh. It normalises now, and the two tests that asserted `true` assert the contract. Four `| 0` truncations are listed in sonar-project.properties instead of changed. Two are a djb2 string hash where the 32-bit wrap is the algorithm. The other two look like plain truncation of a row id, but `| 0` is also coercing undefined to 0 there — the settlement flows the API returns carry no user_id, so that is the normal path, and Math.trunc returns NaN and crashes the lookup. |
||
|
|
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. |
||
|
|
b5c8182410 |
feat(planner): clicking the selected day header again deselects it
Same effect as the day panel's close button: selection cleared and the day detail panel closed. Discussed in #2024. |
||
|
|
1cb67d5e64 |
feat(planner): scope the planned places filter to the selected day
With a day selected, the 'planned' filter now shows only that day's stops, matching how the other filters follow the selection; with no day selected it keeps showing the whole plan. 'unplanned' still uses the whole-trip assigned set. Discussed in #2024. |
||
|
|
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. |
||
|
|
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. |
||
|
|
efcdbd94fb | fix(client): compute "today" from the local calendar, not UTC | ||
|
|
4bac447ac8 |
fix(planner): draw only the selected day's automated transports
Closes #2019 The route toggle belongs to one day; the reservation list it filtered is the whole trip. So turning on a day's routing let every transit reservation in the trip draw its geometry at once, including the ones belonging to other days — on a day that has no transport of its own, which is what the reporter saw. Only automated transports rode it, and that is the distinction they drew: a manually entered train or flight is type 'train'/'flight' and reaches the map only through its own per-booking toggle. Only the transit search stores type 'transit'. Gate the transit branch on the day, and gate only that one — the per-booking toggle stays trip-wide, because switching a booking's route on is a deliberate act about that booking. The predicate is the day-order range, not the departure day: an overnight journey carries end_day_id and has to stay on both of its days, the same rule the sidebar timeline already applies. Day order rather than day id, since ids stop being monotonic once days are reordered. The day context is optional throughout, so a map without one — collections — keeps behaving exactly as it did. Not a 4.0.0 regression: it reproduces on 3.4.1 too. |
||
|
|
6779cd2dd1 |
fix(studio): fit an outline map to the country the trip was in
Closes #2021 A country is one path but not one landmass. France's is five rings, three of them Kerguelen, French Guiana and New Caledonia, so fitting a map to its bounding box asked for 221 degrees of longitude and printed a Paris-Lyon-Nice route as a single dot. Britain does it through South Georgia, Norway through Svalbard, Ecuador through the Galapagos — which is why a Netherlands journey looks perfect and a France one does not. Satellite only escaped by accident: imagery leaves the shape list empty, so the fit fell through to the stops. Same control, silently doing nothing. Fit to the rings the journey is actually near, and keep the whole shape as the fallback for a map with no route yet. The margin is the route's own size with a two-degree floor, which is the judgement in this fix: at two degrees a Paris-Nice route still keeps Corsica, which is the picture that trip wants, while Kerguelen stays out. The outlines themselves are untouched — the overseas rings are still drawn, they just run off the frame the way a coastline does. |
||
|
|
477e44ef50 |
fix(journey): keep the mobile timeline at the bottom of the screen
Closes #2022 The story preview carried both `block` and `line-clamp-2`. Tailwind emits the display plugin after line-clamp, same specificity, so `display:block` won and `-webkit-line-clamp` — which only does anything on a -webkit-box — was inert. The whole entry text rendered, the card grew with it, and the timeline it sits in is bottom-anchored with no height of its own, so it climbed up the screen. Measured at 430x932: three words put its top edge at y=744, four hundred words at y=24, two thousand at y=-2916. That is the reporter's "at the top in one journey, in the middle in another" — it is not the install, it is the longest comment in the journey being opened. The same growth explains the other half. The timeline sits above the map and is a horizontal scroller, so once it covers the screen it takes every drag Leaflet would have got, and only the gap between it and the dock still reaches the map. Dropping the display utility fixes today's cards; capping the card height means no future one can do this again. 140px is what the older journey timeline pins its active card to, so the two look like the same product. Also lift the map attribution over the dock, which the reporter's screenshots show sitting on top of it. Both engines, since a phone meets either. |
||
|
|
44ddfa510e |
fix(client): only give a scheme to something that looks like a host
safeExternalHref prefixed https:// to anything without a scheme, which turns a note that reads "call the hotel" into a link to a host that does not exist. A dot and no whitespace is what separates www.hotel.com from free text. |
||
|
|
7c6a17c020 |
fix(client): make the new rollbacks roll back only what they wrote
The rollbacks this branch added were snapshot-shaped: capture the whole collection before the write, restore it on any failure. That is wrong in two ways at once. It undoes work the server already accepted when a later step is the one that fails, and it throws away whatever a collaborator's socket event applied while the requests were in flight. The day-plan reorder is the clearest case: five awaits, and a failing day-note write reverted transport positions the server had already stored. It now records only the fields it is about to overwrite, per booking, and drops each one as its request lands - so a note failure reaches the catch with nothing to undo, and a booking that arrived mid-write survives. The notification toggle had the same shape underneath: the rollback was made surgical but the request still shipped the whole matrix built from the pre-toggle render, so two overlapping toggles overwrote each other anyway. It sends the one cell that changed. The test pinned the disagreement as correct; it asserts the payload now. Photo reorder awaits allSettled rather than all, so the UI is not snapped back while the remaining writes are still landing - a partial run really does leave the server holding part of the new order, and only a total refusal justifies restoring the old one. Both the desktop editor and the mobile sheet, which was still swallowing every rejection. Also: the geolocation start guard is a run generation now, because a start issued after a stop re-sets a plain flag while the first run is still parked in the iOS permission prompt; a category rename reports once for the batch and rethrows, so the settings dialog stops closing as if it had worked; and a 404'd import job stops telling the user their file was invalid when it had simply expired. The clipboard fallback for plain-HTTP installs was pasted into three files by this branch. It is one helper now, and the two copy buttons in the members modal that still called navigator.clipboard unguarded use it too. Same for the fourth local-date helper written next to the three that already import the existing one. |
||
|
|
818021908f |
fix(client): correct the second pass over this branch's own client fixes
Four of the fixes already on this branch were wrong, and an adversarial read of the diff is what turned them up. The shared merge helper claimed to implement the server's COALESCE(assignment_time, place_time) and did not: it pinned the embedded times to whatever the assignment already showed, ignoring the override column entirely. Sharing that with the WS applier traded one bug for another - a collaborator moving a place from 08:00 to 10:00 stopped reaching the day card for every assignment WITHOUT an override, which is most of them. It merges the way the server projects now, and the test that was supposed to pin this seeded a fixture the server could never produce (embedded times that differ from the pool row with no override to explain the difference). The per-iteration auth gate went into every loop in tripSyncManager except the one that matters most: the mutation-queue replay, which writes server responses straight into Dexie. After a logout that is the shared anonymous database. The fire-and-forget tags/categories writes two lines below a gate added in the same commit were ungated for the same reason - they resolve later, and the check has to happen then. safeHttpUrl is backed by placeWebsiteSchema: http(s) only, capped at 500 characters. A booking url is deliberately free-form - shared's own contract says so in as many words, because people paste bare hosts and long provider deep links - so guarding it with the stricter schema turned a link somebody saved last year into dead text. safeExternalHref refuses the schemes that execute in this origin and gives a scheme-less value https://, which is what an address bar does with the same input. And the memories timeout was 20s while the server's own budget for a Synology call is 30s, doubled again when the session has expired and it re-authenticates: a slow NAS that answered before this branch would have started reading as disconnected. Those calls had no client timeout at all before; they have none again, and the ones that need to give up early carry an AbortSignal. The notes thumbnail also loses its NUL byte cache separator - it made git treat the whole file as binary, so a security fix landed in it unreviewable - and gains the same cache cap its chat sibling got. |
||
|
|
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. |
||
|
|
e72338bbbc |
i18n: the English left in Costs, Studio and the mobile timeline
Three surfaces still had English written straight into JSX in a codebase that gates on key parity: the reworked expense modal, the Studio quickbar and rotation handles, and the leg-mode menu on the mobile day plan. Keyed, and translated into all 23 locales rather than pasted. Also reworded the plugin geolocation permission: it told the user their browser would ask, and it never does - TREK answers the plugin from the page's own location permission. |
||
|
|
27be764164 |
test(client): the coverage that goes with the previous commits
Left out of the commits they belong to by hand, not by intent: the mobile screen suites, the tripRepo cache-first read behind the startup-destination fix, and the MSW handlers the stricter unhandled-request setting needs. |
||
|
|
c75e958fc0 |
fix(client,sdk): the smaller silent failures, and a dead SDK test file
The SDK's parity suite had an unescaped apostrophe in an it() title, so the file did not parse and vitest reported it as "no tests". Both guards in it were dead - including the byte-identical egress-policy check the package's own notes call the worst possible bug in that module to lose. No CI job runs the SDK suite, so it landed quietly; the next npm publish would have failed on it, since prepublishOnly runs the tests. Also in the SDK: the dev preview answered trek:geolocation without checking the geolocation:read grant, so a plugin whose manifest is missing the permission works perfectly in dev and is dead once installed. That is the lenient direction, which is the one that ships broken plugins. Client side: the background-task poll never dismissed a 404'd import job and spun forever, "Update All" collapsed its busy flag after the first plugin settled, the lightbox had no cancellation so a slow signed URL rendered the wrong image, the plugin geolocation watch was torn down by any unrelated settings write, manual date entry accepted 31 February, cover previews were never revoked, and check-all issued one request per packing item. |
||
|
|
b3d8bf08f1 |
fix(mobile): local dates, revoked object URLs, and writes that said nothing
Four screens derived "today" from toISOString(), which is the UTC calendar day: an expense filed at 17:00 in Los Angeles was dated tomorrow, a to-do went overdue eight hours early in Tokyo, and a new journal entry got the wrong day either side of midnight. All four take the viewer's own calendar day now, through the helper the planner already had. Three sheets built preview object URLs in render and never revoked them, so scrolling a picker leaked one blob per pass. Photo reorder fired its PATCHes from inside a setState updater, which StrictMode double-invokes. And several writes failed in silence: the notification toggle, the collab feature switch rolling back to a stale snapshot, the appearance change whose debounced write was dropped on unmount so it reverted on the next reload. The backup-code print page skipped the escaping its desktop twin does, the clipboard copy threw on a non-HTTPS install, and reservation metadata was JSON.parsed unguarded in render, which takes the whole route down. |
||
|
|
19745b7b0d |
fix(studio): keep "Keep mine" from throwing the document away
After a save conflict, Keep mine set the version and unblocked writes but never re-queued the local document, and write() had already cleared the queue before the request. So if the user picked Keep mine and then simply closed the editor without typing anything else, saveNow() found nothing pending and did nothing: their afternoon of edits existed only in memory, the indicator said idle, and the server kept the other version. The exact outcome the conflict dialog promises to prevent. Ctrl+Z was intercepted while typing in a text field too. Studio commits text on blur, so mid-edit the typing is not in the undo history yet: the browser's own text undo was suppressed and a previous layout change was undone underneath the open editor instead. Reads as random data loss. Also: delete no longer removes locked elements, the auto-layout no longer freezes an empty GPS path when tracks resolve after the one-shot build, the back-cover tally cannot print "0 places", the map fetch aborts when Studio closes, and a double Enter in the list-name field stops creating two lists. The page-pattern gate had quietly stopped covering anything: the Desktop-shim refactor moved every real container out of the default export the checker reads, and its regex missed useRef<T>(null) anyway. Widened on both counts, and the state it immediately found moved into the hooks. |
||
|
|
7959470802 |
fix(collab): scope the realtime handlers to the trip, and stop losing writes quietly
The notes and polls WebSocket handlers applied every event they saw without checking which trip it belonged to, so an event for another trip the user is a member of landed in the open one. The poll load effect had no cancellation either, so switching trips fast let the old response win. What's Next compared trip-local calendar dates against a UTC one while building the time from the local clock. West of UTC that means the rest of today disappears from the widget every evening and tomorrow's items are labelled Today; east of UTC it happens after local midnight instead. A note website was rendered as a raw href with no scheme check, the same stored-javascript: shape the planner had. Category rename walked the notes in a loop with no handling for a partial failure. And the school-holiday region labels resolved German first for every locale. |
||
|
|
61d4fd4d64 |
fix(planner): scheme-check the booking URL, and stop the serial routing calls
A booking's url is rendered as a plain href to every other member of the trip and nothing ever checked its scheme, so a collaborator could store javascript: there. The place website next to it has been going through safeHttpUrl for exactly this reason, with a comment saying stored values predate the server check - this is the surface that was left. Both ends now: safeHttpUrl at the render site, and the scheme in the shared contract so nothing new gets stored, additively so existing rows still load. The per-leg routing rewrite turned one multi-waypoint request per run into one serial request per leg. A twelve-stop day is eleven round trips to a public OSRM in sequence before a single connector appears, and toggling km/mi does it all again. Group consecutive legs that resolve to the same mode into one request and fire the groups concurrently, keeping leg order on the join. Transport reordering wrote to the store and persisted with a bare catch, so a failed PUT left the sidebar showing an order the server never got; it reconciles now. Plus the smaller ones: the OSM menu_url rendered unchecked, expand/collapse-all persisting to a different storage than the day list reads, the debounced address search with no ordering guard, the accommodation save chain with no error path, and window.__trek_map holding a destroyed map. |
||
|
|
47a4276e84 |
fix(client): make the offline core survive a logout and stop clobbering itself
logout() unregistered every sync trigger, and the only place that registers them is App's mount effect - which never runs again, because logout navigates through the router rather than reloading. Log out, log back in, go offline, edit something: it queues and nothing ever flushes it. No online listener, no visibilitychange, no 30s heartbeat, no WS reconnect hook. Register on auth success instead; registerSyncTriggers is already idempotent. The background sync loops only checked the auth gate on entry. Logout swaps the Dexie handle to the shared anonymous database, so a trip loop or a blob download still in flight afterwards wrote the logged-out account's itinerary and documents into it - the database that survives logout and greets the next person on a shared device. Check the gate per iteration. And the remote place:updated applier replaced the whole embedded place, while the local reducer right next to it preserves the assignment's own times. Same event, two answers, and the remote one wipes a scheduled 09:00-10:30 off the itinerary when someone else renames the place. One merge helper now, used by both, so they cannot drift again. The test that should have caught it seeded null times and asserted only the name. Also: moving a day note deleted before it created, and restored the note only locally when the create failed; t() interpolation was corrupted by a '$' in a parameter value; a 429 upload was treated as permanently failed; and the geolocation watch was started inside a state updater. |
||
|
|
39c8a22352 |
fix(offline): replay queued writes an interrupted flush left behind
A flush marks each row 'syncing' before it sends, and only ever picks up rows that are 'pending'. Kill the tab mid-request - the OS evicting a PWA does exactly this - and the row stays 'syncing' forever: it keeps counting towards the pending badge, but nothing ever retries it, fails it or shows it as a conflict. Anything queued behind it that depends on it later fails with 'unresolved temp id'. Stamp the row when it goes in flight and reclaim it on the next flush. The axios instance times out at 8s, so a minute is well past anything that could still be running, and the mutation id doubles as the idempotency key, so replaying a write the server already applied is answered from its cache. |
||
|
|
e91ad69c83 | fix(atlas): visual empty stats state | ||
|
|
bb7c484ca4 |
feat(notices): add the 4.0.0 release modal
The thank-you modal was one column of markdown for every notice. A release needs more room than that: what shipped, and why it took what it took. Adds a `release` block to the notice contract. A notice carrying it renders through ReleaseNoticeModal instead of the generic body — the release on the left, the note from the maintainer on the right, both halves driven entirely by translation keys so the next release only edits the registry entry. Sizing lives on one CSS variable (--rn-s), so the panel scales as a piece and the design's proportions hold at every breakpoint. The thank-you notice gets maxVersion 4.0.0: from there on the release modal carries the same thank-you and the same two support links, and installs still on 3.x keep the old one. All 23 locales. |
||
|
|
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 |
||
|
|
942e1932c9 |
fix(budget): name the category parking and file parking bookings in it
The key was 'parkings', which is not a word and does not match the 'parking' the reservation type uses everywhere else. The shared trip page prints the stored category verbatim, so a share link would have shown "parkings" in every language. The category also had no relationship to the booking type of the same name: RESERVATION_TYPE_TO_COST_CATEGORY still sent a parking booking's auto-expense to Transport, so the new bucket sat empty next to the costs that belong in it while a manually typed "parking" resolved into it. Both paths agree now. |
||
|
|
999236f944 |
feat(budget): add "Fuel" and "Parking" expense categories
- Adds two new fixed budget categories: fuel ("Fuel") and parkings
("Parking"), alongside the existing set (Accommodation, Food & drink,
Groceries, Transport, Flights, Activities, Sightseeing, Shopping,
Fees & tickets, Health, Tips, Other).
- Icon/color metadata added for both (Fuel and ParkingCircle from
lucide-react), plus legacy free-text aliases (gas/petrol->fuel,
parking->parkings) for existing/imported expense rows.
- Label strings added for all 23 supported locales.
- Adds test coverage for the new categories and legacy alias mapping.
|
||
|
|
875f87b885 |
fix(planner): open the booking a place is linked to
The place inspector summarises the booking attached to the day assignment and then went nowhere: the strip is a bare div with no handler, no role and no tabIndex, and PlaceInspector was never handed an editor callback in the first place. Reached from a map marker, that strip is the only route a restaurant or tour booking has — only transports with two positioned endpoints get map pins of their own — so its fields and its attachments were unreachable. The identical lookup in the day sidebar has always carried a pencil that picks the editor by type. The inspector now takes the same onEditTransport / onEditReservation pair and does the same: a ferry, a flight, a train opens the transport form, everything else the reservation modal. That split matters — ReservationModal has no transport entry in its type list, so a ferry sent through it would have its type re-picked on save. Each handler arrives as undefined when the user lacks the matching right, which is what keeps the strip a read-only summary instead of turning it into a button that does nothing. Gating inside the handler would have re-shipped the reported symptom with a label claiming otherwise. The phone place sheet never rendered the strip at all, so the same booking was just as unreachable there, only invisibly. It shows it now and opens the same two editors, closing itself first — it hangs off the place selection rather than the sheet stack, so closeSheet() would leave the editor stacked on top of it. Closes #2012 |
||
|
|
a23a81f35b |
fix(studio): the spread import points inward too
Missed in the sweep: "Import spread" opens a JSON file picker and wore the outgoing arrow like the rest of them. Refs #2006 |
||
|
|
331954c1b0 |
feat(mobile): drag to reorder the day plan, and reach the day sheet from any day
Reordering a day on the phone meant two 15px arrows, while the same list on the web takes a drag. It takes one here too now, via the long-press bridge tablets already use (#1616): it watches only draggable rows inside an opted-in container, waits out a press before claiming the gesture — so a swipe stays a scroll — and replays real drag events, which is why the rows only needed the receiving half. Armed in edit mode and on coarse pointers alone, so go-mode taps and map pans are untouched and the polyfill retired in #1432 stays retired. The arrows stay: they are the accessible path. Both paths go through one mover, so the chronology guard and the undo entry are the same either way. The card header was also the only route into the day sheet, and it rendered only when the day happened to have an accommodation or a weather reading — with the accommodation chip as the sole tap target. A day with neither had no way in. A day pill now leads that row unconditionally, the weather reading is a target too, and the active day chip carries a chevron so the second tap that opens the sheet is visible in map view as well. Closes #1997 Closes #2004 |
||
|
|
2d6c8c5157 |
fix(mobile): open the stay when you tap the stay
Tapping the accommodation in the day sheet opened the place it is pinned to, so check-in, check-out and the confirmation code had no way in at all. The sheet that edits them has taken an accId since the mobile rewrite; nothing ever passed one. The row is a real button now and opens that editor, with the place kept one tap to its right for anyone who wants it. Viewers without day_edit keep the place as their target, since the editor is no use to them. The linked hotel booking below opens too — the day's booking list filters hotels out, so that strip was the only place it showed and it did nothing. Closes #2001 |
||
|
|
da92581a16 |
fix(planner): let a route choice survive the map, and keep new items on their day
Two things the mobile planner forgot. The day sheet's route toggle only ever lived in component state, and entering the map — or tapping a day while in it — forced it back on. Turning the route off therefore did nothing you could see and had reset itself by the time you looked again. The choice is now remembered per trip; the map still switches it on by default, but only while the user has never said otherwise. Adding a place or a booking from inside a day did not connect it to that day. The place dropped into the unplanned pool, which on mobile is a different screen — it looks like the save was lost — and the booking opened with no date at all. Both entry points now carry the day they were opened from, including a long-press on the mobile map, where the chip rail names a day at all times. The desktop map has no such context and passes nothing, so its pool behaviour stands. While here: the import button on the bookings and transports headers only appeared when kitinerary was installed, though the server also accepts an import when the LLM parser is configured (#2007). Closes #2003 Closes #1998 |