mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
942e1932c90f0db907686dcdc2e88ec71e2b985e
1943 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
8b88986ca8 |
fix(bookings): make the reservation import findable on the desktop panel
The import is not missing from the desktop — it sits at the far right of the bookings toolbar, next to the accent "Manual Booking", above an empty page with nothing but the mascot on it. On a fresh trip that is exactly where nobody looks, which is how it reads as absent. The empty state now offers the ways in that apply: add manually, import from a file, and AirTrail where it is connected. EmptyState grows an optional action slot for it, so every other caller is untouched. Closes #2007 |
||
|
|
9094f05604 |
fix(planner): close the accommodation loop when no times are recorded
With "optimize route from accommodation" on, the drawn route still started at the first activity on any arrival day and stopped at the last one on any departure day. The two bookend rules ask whether you can be proven to have been at the hotel already — but they read a missing check-in time as proof against, and the hotel picker leaves that field blank by default. So the loop hung open at one end on every boundary day, and at both ends for a stay booked on a single day. With no time recorded there is no bar to clear, so the leg is drawn. Where a time *is* set the existing rules are untouched: a stop before check-in still draws nothing (#1465), and a transport arrival is still not a hotel departure (#1321). Closes #2009 |
||
|
|
5e78069223 |
fix(journey): saving a suggestion turns it into an entry
A trip visit shows up in the journey as a suggestion, and opening one puts you in the entry editor. Pressing Save there only promoted it when a story, a photo or a linked photo had been added — a title, a mood, the weather or a set of tags all left it a suggestion, and so did saving it untouched. From the outside that looks like the save did nothing. An explicit Save is the user saying this is their entry now. handleSave only ever runs from the Save button, so there is no autosave to misread as intent. Closes #2008 |
||
|
|
df139a2d96 |
fix(ui): point import controls inward and give Google Maps its own mark
Every import button in the client wore an outgoing arrow, so the control that pulls a booking or a place list in looked like it was about to send something out. The places-import sheet managed to contradict itself inside one screen: Download in the header, Upload on the row below it, Upload again on the file step. Imports now take Download, or FileDown where the button opens a file picker; genuine uploads and the desktop drop zones keep Upload. The mobile places-import entry point wore an ellipsis, which promises a menu. The "open in Google Maps" buttons drew the four-segment Google *search* G, flattened to one colour — a glyph that reads as "run a search". They get a map pin instead, factored into one component so the desktop and mobile copies stop drifting apart. Closes #2005 Closes #2006 |
||
|
|
ec3017f9cc |
fix(ui): keep dropdown panels anchored to their trigger
The three fixed-position popovers computed top/left inline while rendering, which reads the trigger's rect exactly once. Scroll a sheet afterwards and the trigger walks off while the panel stays nailed to the viewport; on iOS the keyboard slides up over a panel that was placed before it existed. useAnchoredPosition measures in a layout effect and re-measures on scroll (capture phase, so the sheets' own scroll containers count), on resize and on visualViewport changes. It also reports how much room is actually left, so a list opened next to the keyboard shrinks instead of hiding underneath it. CustomDateTimePicker clamps a fixed 268x360 calendar horizontally, which no generic box can express, so it takes the bare re-measure signal instead. Closes #1999 Closes #2000 |
||
|
|
e753603ec8 |
test(db): re-point the trailing-migration guard at the new last slot
The guard proves that a migration appended to the end still runs on an install that is one version behind, by undoing what the last one did and replaying it. Its own comment asks whoever appends the next migration to re-point the undo, which is what this does: the trailing slot now clears the GB rows out of place_regions rather than creating journey_books. The Spanish row alongside it is there to catch the opposite mistake — a migration that clears more than its own country. |
||
|
|
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 |
||
|
|
f574e75dbe |
fix(pdf): print a day in the order the plan shows it
The export ordered each day by itself, on day_plan_position — one global number per booking, seeded automatically from the place count of whichever day happened to render first. On any other day of a span it therefore pointed at the wrong slot, and where it was still null the export fell back to the order the API returned rows in, which is reservation_time ASC with nulls first. That is why the same trip printed correctly some of the time and swapped two bookings the rest of it, and why the reporter's workaround worked: adding a place and removing it again reseeded the position from the day they were looking at. The plan has never used that column. It picks transports per day, gives each one the time that day shows, inserts them among the places and re-sorts the whole day by the clock — and none of that was reachable from here, because this file had its own copy of the ordering with subtly different rules. So the copy is gone and the export goes through utils/dayMerge, the same functions the plan uses. Two differences fall out of that, both of them the plan's existing behaviour: a booking attached to a place is no longer printed twice, and a multi-leg journey prints one row per leg at each leg's own time rather than one row carrying every leg as a subtitle. The per-segment booking code #1943 added is read back from the booking, since the leg object does not carry it. Closes #1978 |
||
|
|
f76ee87336 |
fix(nav): stop the tab pill from running under the logo and the user menu
The centred tab menu was absolutely positioned, so it had no relationship to what sat beside it. Its width grows with every enabled addon and every page plugin, and once it outgrew the free space in the middle of the bar it simply ran underneath the logo on one side and the bell and user menu on the other. The only adaptation was a hard-coded 1024px breakpoint that drops the tab labels — a constant chosen when two or three addons was the whole story, and one that cannot know a plugin has added anything. So the bar is three columns in the flow now: the brand block and the action cluster carry equal weight either side, and the pill sits between them. That keeps it centred on the bar as before, and makes overlap something that cannot happen rather than something a breakpoint has to predict. With min-w-0 the pill gives way and scrolls when even it runs out of room, instead of pushing the actions off the edge. Measuring the pill against the free space and folding the overflow into a menu was the other option. It buys a tidier look at a narrow width and costs a ResizeObserver on every desktop page, a cache to keep the measurement from oscillating against the mode it chose, and a new string in 23 locales. Not for a fix to overlapping. Closes #1983 |
||
|
|
bb0d65bb19 |
fix(map): read the last fitBounds call without Array.at
The client's tsconfig targets a lib without Array.prototype.at, so the two new assertions typechecked locally against the test runner but not against tsc. Index arithmetic instead. |
||
|
|
7aead0b713 |
fix(map): stop the day fit from framing everything and then drifting off it
Selecting a day compensated for the day panel twice: the fit reserves the panel's height in its bottom padding, and then 300ms later the camera was nudged again with panBy([0, 150]). Both came from the same change, and together they moved the view 150px further than intended. Wide days never showed it. A day that runs north to south does: the fit is height-bound there, so the northernmost stop sits exactly on the top padding line, and the nudge then pushes it off the canvas. What the user sees is the map framing the whole day correctly and then sliding upwards, leaving the first marker outside the view. The padding alone is right, and it is what MapViewGL has always done with the same numbers, so this also brings the two renderers back into agreement. Closes #1982 |
||
|
|
d0cc4e2f00 |
fix(planner): give the day's optimize button room by dropping its label
Every button in the day's route-tools row hands the day somewhere: show the route, open it in Google Maps, open it in CoMaps, reorder it. Two were already icon-only. Optimize carried a label on flex: 1 with padding '6px 0', which is no horizontal padding at all — survivable until CoMaps added a fifth button, at which point the text sat hard against both edges of its own button. Icon-only now, matching the two beside it. The label stays as the accessible name and the tooltip, so nothing about reaching the action changes, and the row has room for whatever gets added to it next. Closes #1981 |
||
|
|
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 |
||
|
|
68bf5316ad |
fix(planner): stop an empty reservation event from breaking a trip for good
reservation:created and reservation:updated come in two shapes. Normally they carry the reservation; the accommodation cascade sends them empty, as a "something changed, go and look" ping that the event contract allows for. The applier read the id off both. With no reservations loaded yet the existence check never ran its callback, so the cast slipped through and the store became [undefined]. From there every render that walked the list threw "Cannot read properties of undefined (reading 'endpoints')" into the route error boundary, and since the store is rebuilt from the same events, the trip stayed unreachable in that browser. The reporter could only get out of it by clearing localStorage. Both appliers now decline an event with nothing on it, and the Dexie writer does too rather than leaning on a swallowed catch. The predicate that walks the list is total as well: it guarded the property but not the element, and it is handed straight to Array.filter over a list it does not own. Declining the empty ping would have left a hotel booked by one member invisible to the others until they reloaded, so the ping is honoured the way day:reordered already honours its own — fetch the authoritative list. The server keeps sending what it sends. Making the cascade carry the full reservation would mean moving the REST, MCP and RPC paths together with their pinned tests, and would still leave the client fragile against the next emitter that does this. Closes #1979 |
||
|
|
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. |
||
|
|
736599a89b |
feat(journey): TREK Studio — a photo book designer for Journey (#1973)
* fix(photos): serve local photos root-relative so a dotted install path works
res.sendFile(absolutePath) resolves against the rewritten req.url under the Nest
ExpressAdapter and answers a spurious 404 even though the file is there — the
same trap files-download.controller.ts:60 and plugin-frame.controller.ts:74
already work around with an explicit { root } + basename. The photo resolver
never got that treatment, so every thumbnail and original 404s on any install
whose uploads path contains a dot segment (~/.local/share/trek and friends).
The public journey asset route had it too, which took shared journeys with it.
The guards above each call already confine the path to the uploads tree, so the
root form is no weaker than what it replaces.
* feat(shared): add the book document contract for TREK Studio
Millimetres throughout, not pixels: a pixel means nothing to a print shop and
changes meaning with the device pixel ratio. CSS maps 1in onto 96px, 25.4mm and
72pt with a fixed ratio, so the editor can render the document at a scale and the
print renderer at 1:1 and land on the same page.
Paint order is the array order rather than a zIndex field that can disagree with
itself, text elements may stay bound to the journal entry they came from, and a
spread can park content a layout has no room for instead of dropping it.
* feat(journey): open TREK Studio over the journey
Studio is a child route of /journey/:id and portals itself above it, so the
journey stays mounted and shows through the panel's 16px margin — it reads as
something opened on top of the journey rather than a place you navigated to. The
URL exists for reload, the back button and shared links; the user never sees it.
Every rule in studio.css is scoped under .trek-dash on purpose: dashboard.css:95
resets button font, colour, background and padding at a specificity a bare .st-x
loses to, so an unscoped rule silently does not apply to a button.
Escape clears the selection and only leaves on a second press. In a dialog
Escape means go away; in an editor that reflex would cost you the page.
* feat(journey): build, edit and arrange the book in Studio
The auto mode turns a journey into a book once and then gets out of the way: what
it produces is an ordinary document, every element as editable as one placed by
hand. Templates work the same way — applying one pours the spread's existing
photos and text into the new arrangement and parks whatever will not fit, so
trying a text-only layout does not cost you the pictures.
Moving and resizing are hand-rolled on pointer events. The drag-and-drop
libraries in this space move items between lists and have no notion of a free
frame, a handle or a magnetic edge; HTML5 drag hands you a ghost image you cannot
control and no position until the drop. It is used for exactly the one shape it
suits — dragging a photo out of the content panel onto a frame.
Selection chrome carries two rings, dark inside light. A single line vanishes
against a photograph of its own value, and over a picture that is bright on one
side and dark on the other no measurement of the background can be right.
The sheet renders in DOM rather than on a canvas so the same component tree can
be what the print renderer runs — one renderer, nothing to drift against. Edit
affordances live in a layer above it and can never reach the page.
* i18n(journey): studio strings across all 23 locales
The empty-frame prompt breaks per language rather than copying the English line
break onto everything — forced onto Japanese or Arabic it strands a fragment on
the second line.
* refactor(journey): keep SpreadView to components only
FONT_STACKS and photoSrc move to their own module: a file that exports both
components and constants breaks Fast Refresh, and this is a file you edit
constantly while designing a page. Types come from the journey store instead of
any, and the count beside the content tabs is a badge rather than competing with
the label for the same weight.
* feat(journey): more shapes, decoration and a content search in Studio
Six shapes now instead of three — rounded rectangle, triangle and an outline-only
box join the rest, plus a quotation mark and an outlined circle under decoration.
A triangle is a clip path rather than the old CSS border trick: that hack cannot
take a fill gradient or a stroke and its size does not follow the frame the way
everything else does. A stroke sits inside the frame, or the drawn shape would be
wider than the box the editor snapped and the print would not match it.
The content panel gets a search over photos and entries. A photo matches on its
own caption and on the entry it hangs from — most photos carry no words at all,
so matching captions alone would look broken on exactly the journeys that need
it. Plain filtering over what is already in memory; a journey holds tens or
hundreds of items, not thousands.
Dates in the panel and in the book now follow the app's language through the
house formatDate rather than the browser's locale, and the entry's date and place
are two badges instead of one dot-joined string the eye has to parse.
* @
feat(journey): a shape library, picture frames and photo grids for Studio
Studio had six shapes, two frames and no grids. This is the library that
makes it usable for a book rather than for a diagram.
Shapes are SVG paths now, not clip-paths. That was forced rather than
chosen: a clipped div cannot take a stroke — the clip cuts the border in
half lengthwise — and a heart is not a box with a radius. Rectangles and
ellipses keep their old div rendering, since a box genuinely is better as
a box and every document written so far contains those two.
Two decisions in shapes.ts are worth stating.
The path *points* are scaled, not the coordinate system. Fitting a path
with preserveAspectRatio="none" scales the stroke with it, so a 0.5mm
outline on a frame twice as wide as it is tall comes out 0.7mm on the
sides and 0.35mm top and bottom. Printed next to a straight rule at
300dpi, you see it. Hence the deliberately boring path subset: absolute
M/L/C/Q/Z, numbers alternating x and y, no arcs.
And every shape fills its 0..100 box exactly, enforced by normalise() at
module load rather than asked for in a comment. The editor draws its
selection outline, its resize handles and every snap target from the
element frame, so a shape occupying 86% of its own frame cannot be
aligned against anything you can see. It costs a regularity nobody can
spot — a hexagon in a square frame still reads as a hexagon.
Frames are the same list: a picture frame is a shape with a photograph
inside it, and keeping two would mean a heart that exists as one but not
the other. Photos gain a mask and a decoration, kept independent, plus
the Polaroid, film and tape treatments that are not expressible as either.
Grids take their gutter out of the cells rather than adding it between
them, so a grid asked to fill the safe area fills it instead of
overhanging by one gutter.
@
* @
feat(journey): derive what a journey adds up to
GET /api/journeys/:id/stats — distance, days, steps, photos, places, the
furthest point, the countries in visit order, and the route itself. Read
only and derived: nothing here is stored, it is what the trips added to
the journey come to.
The route comes from the journey entries where they carry coordinates,
and falls back to the places on the linked trips where they do not —
which is the common case for a journey assembled from trips nobody has
written up yet. The two are never mixed: an entry written about a place
that is also on the trip would be two stops at one location, and the
distance would count that leg twice.
Countries read place_regions first, which Atlas already fills, and fall
through to getCountryFromCoords for the rest. That import is a plain
function from a module built without DI or Nest edges, so it costs the
journey domain no coupling to Atlas.
Distance is great-circle metres, not driving distance: there is no
routing engine here, consecutive stops are not necessarily joined by a
road, and a book that prints a road distance it never measured is worse
than one that says "as the crow flies". Metres because kilometres and
miles are a display choice.
A hotel across three nights is one place with three day assignments, so
the place query aggregates back down to one row at its earliest day —
otherwise the route would visit the hotel three times.
Two test fixes came out of writing this. journey.controller.test.ts still
asserted an absolute path from res.sendFile, which the photo fix in this
branch had already changed to the basename-plus-root pair; and
linkTripToJourney in the factories wrote to linked_at, a column that does
not exist — it had no callers, so it had never once run.
@
* @
feat(journey): travel elements — the route, the figures, the countries
A photo book of a trip is not an album: the trip carries facts, and those
facts make pages photographs cannot. Four new elements — a vector route
map, a figures panel, a country page and a set of small marks — plus the
mood, weather and pros/cons an entry already records.
Every one of them carries its own values, resolved when the element is
placed and written into the document. The print renderer is headless
Chromium loading a page; one that needs an authenticated fetch to know
what it says will print blank the day a token expires. It also makes a
book a record of a trip as it was, rather than something that quietly
rewrites last year distance when a stop is added. travelRefresh compares
what is stored against the journey and offers an update, so bringing an
element up to date is a choice.
The map is vector, from the coordinates, over silhouettes simplified
offline from the admin-0 boundaries the server already bundles for Atlas
— no licence question, nothing to fetch at render time, sharp at any
print size. It paints no background: a map is geography sitting on the
paper, not a window cut through it, so it can be laid over a photograph.
Flags are drawn rather than typed. A regional-indicator pair is the
obvious answer and works everywhere except Chrome on Windows, which has
no flag glyphs and renders the two letters instead — "DE" in a printed
book is not a flag. Thirty-three flags are exact constructions; anything
with an emblem or unequal bands is deliberately absent and falls back to
the country outline, because a wrong flag is worse than no flag.
Panel tiles render the real element small, through the page own
renderer. These differ from each other by what they say rather than by
their shape: four map styles are four maps, and no icon can tell you
which is which.
@
* @
feat(journey): manage spreads from the pages rail
Add, insert between two spreads, duplicate, reorder and delete. None of
it existed — a book was whatever the auto layout produced.
The cover and the back cover are fixed points: a book has exactly one of
each, they are single pages, and no sequence of clicks should be able to
produce a book with two covers or none. Every operation asks one
predicate whether a spread is inner, rather than each caller testing role
for itself, and a spread inserted after the back cover lands before it.
Duplicating re-ids every element it copies. Two elements sharing an id
would make selection ambiguous and every lookup that follows it wrong,
delete included.
Also: a locked element can be selected again. Locking used to switch its
hit target off entirely, which was a dead end — it could not be picked,
so the inspector never showed it, so the unlock button was unreachable
and the only way back was undo. Locked now means what it says: it cannot
be moved, and its resize handles are hidden so the editor does not offer
a drag it will refuse.
@
* @
feat(journey): let the auto layout use the travel elements
The book now opens on a summary spread — the route drawn on the left, the
figures on the right — and, for a journey that crossed a border, a country
page after it. Both are omitted rather than left empty when there is
nothing to draw: a spread holding a blank frame over the words "0 KM" is
worse than a shorter book, and every page in a printed one costs money.
Only the figures the journey actually has make it onto the page. A zero
is not a fact worth a tile, it is a hole in a composition.
Each entry gains a short accent rule above its heading — one drawn
element, and the cheapest thing here that makes a spread read as designed
rather than as text placed on a page. An entry with no photographs, which
is the one spread with a blank left page, sets its date as a numeral
there: a gap in the book becomes a pause in it.
The back cover closed with `${places} Orte` — German, hardcoded, in a
book that follows the app language everywhere else. It is a figures
element now, so it takes its labels from the same translations as the
rest of Studio.
The layout waits for the figures rather than racing them. Laying out
first would give a journey a summary spread or not depending on which
promise resolved first, and Studio already shows its loading state until
the document exists, so the wait costs nothing.
@
* @
feat(journey): the Studio wordmark replaces the book-name field
The field asked for a second name for something already named after the
journey — a box whose only real use was being left alone. The bar says
what this is instead: the lockup, inline so it takes the theme ink
through currentColor rather than needing a light-on-dark copy, and a beta
mark beside it.
Along with the strings for everything in this branch, across all 23
locales, and the two keys the name field used, removed rather than left
as orphans nobody can tell from live ones.
journey.studio.frames was "Picture frames" — too long for a tab sharing a
row with two others, and untranslated in 21 of the 23 locales, which
showed the English through. It is one word now, in every language.
@
* @
feat(journey): a free trim size, page numbers, and auto layout as a button
Three from the list the PR left open.
**Custom trim.** The five presets are what vendors actually bind, which
covers almost everyone — "almost" being the problem. A local printer with
a 24x18 press, or a book meant for a particular shelf, could not be made
here at all. Two fields under the format list, and typing in them *is*
picking custom: a book that is 210 by 240 is not square-21 with a typo,
so the preset follows rather than needing to be chosen first. Switching
to custom keeps the page it is on — it adjusts the format you have.
**Page numbers.** Set on the document and drawn by the renderer, which is
the only place that knows what number a page is. Stamping them with
pdf-lib afterwards would print numbers the editor never showed, and a book
you cannot proofread on screen is not much of an editor. They are also the
one thing on a page that is not an element: an element holding "14" would
still say 14 after its spread was moved. `startAt` exists because the
first inner spread is rarely page 1 once bound, and outer/inner is a real
distinction on a spread — getting it backwards puts both numbers in the
fold.
The controls live in the inspector empty state, which was one line of
hint text. With nothing selected, that panel is where the book own
settings belong.
**Auto layout as a button.** It ran once, on open, which made it the one
thing in Studio you could not ask for; a page that came out wrong had to
be rebuilt by hand. Now it does one spread or the whole book, both as
ordinary commits — the first thing anyone does with a button like this is
press it to see, and the second is want their page back. A single spread
keeps its id so the rail does not scroll, and refuses on anything not
built from an entry rather than replacing it with a blank.
@
* @
feat(journey): cover layouts, and five more for the spreads
The cover had none. The panel said covers are designed by hand, which was
true and unhelpful — it is the page people care most about and the one
they had no starting point for.
A cover is a different problem from a spread and gets its own set rather
than a filter over the existing one: it is a single page, the type is the
subject rather than a caption to a photograph, and a spread layout applied
to it lands half its frames past the edge. Five of them, from full bleed
to type-only, and the panel serves the right set for whichever spread is
open. The cards scale as one page there, and drop the fold line, since
there is no fold.
Five more spread layouts as well — a picture with a narrow column of
story, nine up, a mosaic around one large frame, two pictures with the
words between them, and four staggered so a row of squares stops reading
as a contact sheet.
Applying a layout also stopped mangling a single text. The old rule took
the longest text as the story and looked for a heading among the rest,
which reads a three-text spread correctly and gets a one-text cover
exactly wrong: the 30pt title *is* the longest text, so it was the story,
so a cover layout with a heading slot and no body slot had nowhere to put
it and parked the title. Size decides the heading now; length only sorts
what is left.
@
* @
feat(journey): seven self-hosted book faces, and a picker that shows them
The serif slot was Georgia. Georgia is a system font — on most desktops,
on no Linux container — so the print renderer would have substituted
something else and a book proofread in one typeface would have printed in
another. The existing Journey PDF export has exactly this bug today,
asking for Inter while only Poppins is bundled.
Every family is self-hosted through @fontsource now, all OFL, all loaded
with Studio own chunk so a face cannot arrive after the text has been
laid out and reflow the line endings someone was setting. Two sans, three
serifs from bookish to high-contrast, two display faces for a cover.
Every stack ends in a generic rather than in another named font nobody
shipped.
The three original ids are kept by name, so documents written so far
still parse — they point at bundled faces rather than at whatever the
machine happened to have.
The picker sets each family in itself. A row of chips reading
"Poppins / Lora / Bebas" in the interface font tells you their names and
nothing about the only thing anyone is choosing between.
Weights a family does not ship are disabled rather than offered: asking
for bold on a single-weight display face gets a synthesised one, which
prints as a smeared regular. Picking such a family moves the weight to
the nearest it really has.
@
* @
feat(journey): real map imagery, from the providers TREK already has
The map element drew country outlines and nothing else. That is still the
default and still the right one — nothing fetched, nothing licensed,
sharp at any print size. But an outline of Iceland cannot show which road
you took, and some books want that.
So two more sources, both reading the tile template and Mapbox token an
admin already configured for the planner and Atlas. A second place to
paste a token is a second place for it to be wrong, and a source with
nothing behind it is not offered at all — showing "Mapbox" to someone
without a token produces a grey rectangle and no explanation anywhere.
Both costs are stated rather than hidden: tiles are fetched when the page
renders, and they are raster, cut for one zoom, so printing much larger
than they were fetched goes soft.
Attribution is neither optional nor a setting. The licence requires
credit and a book is a published work — nobody can add a footnote after
it is bound. The credit is matched on the host, so an instance running its
own OSM mirror still credits OpenStreetMap, and an unrecognised source
gets none rather than an invented one. It is written into the document
when the element is placed, so the renderer cannot lose it.
The route is projected onto the tile grid when there is one. The vector
map fits itself to country outlines, and a line placed by that fit sits
next to the road rather than on it — the sort of error that only shows up
once it is printed. Outlines are dropped under imagery, where they would
be a second coastline over the one in the picture.
@
* @
fix(journey): make the Studio type-check again
The map element used tileView and projectOntoTiles without importing them,
useMapSources declared a stricter frame type than any real frame satisfies,
and the mood icon read a colour through an `in` check that types as unknown.
The page fixtures in the studio tests predate page numbers, so they build a
BookPageSetup that the contract no longer accepts — now parsed from the
schema, which cannot drift from it.
@
* @
feat(journey): the book is saved
Studio laid a book out on every open and kept it in the tab. Close the tab
and the afternoon went with it, and the auto layout was not a starting point
but the only point.
The document now lives in journey_books, one row per journey, and autosaves:
quiet for a moment after the last change, or at the latest fifteen seconds
in, so a drag across a spread is one request rather than four hundred. A
stored book wins over a fresh layout on open, and closing the editor writes
whatever is still queued.
Everyone on a journey can edit its book, so the write carries the version it
was made against and the update matches on it in a single statement. A save
that has been overtaken is refused with the current record in the body,
which turns "your work vanished" into a choice between two versions. The
server broadcasts journey:book:saved to the other editors — the version, not
the document — and a client with nothing outstanding takes the new one. A
client with unsaved edits deliberately takes nothing and conflicts on its
next save instead.
@
* @
feat(journey): export the book
The one thing Studio could not do. The button was there and disabled.
A book now goes out as sheets: every inner spread cut down the middle into
leaves in reading order, bleed carried out past the trim on all four sides,
crop marks outside that saying where to cut. The cut is a window rather than
a re-layout, so a photograph crossing the gutter still meets itself once both
halves are printed. Spreads stay whole in the other mode, which is what you
send someone to read.
The browser makes the PDF, from the editor components at 1:1. Considered and
rejected: headless Chromium on the server, and a PDF library drawing the
pages a second time. TREK is self-hosted, and neither Chromium in the image
nor a second renderer to proofread against is worth carrying when the browser
already produces exactly what the editor shows — it is also how the other
exports here work. What the browser will not write is PDF metadata and the
TrimBox entries; the marks carry that instead, as they have since long before
the format existed.
Two things had to give. `SpreadView` clipped at the trim, which is right on
screen and leaves a white hairline down every edge of a bound book, so print
mode lets the page run out and the sheet does the clipping. And the print
document is its own document: it carries the app stylesheets across so the
seven bundled faces are the ones that print, and waits for images and fonts
before it offers to save.
@
* @
fix(journey): make the Studio popovers cover what they cover
The auto-layout menu sat on the glass tokens, which carry 42-58% opacity when
the user has transparency on. That reads as depth over the workbench, where
the rails and the bar use it, and as a mess over the inspector, where this
menu opens: two layers of text through each other, neither readable.
The export dialog was worse — it named --panel and --card, which are not
tokens here, so its background resolved to nothing at all and the whole thing
was see-through.
Both are opaque now. The other glass surfaces are left alone: they float over
the workbench, and that is what the material is for.
The print preview also fits the pane. A spread sheet is over 400 mm across,
so at 1:1 the book ran off the side and had to be read by scrolling sideways.
Screen only — the print stays at full size.
@
* fix(journey): a cover keeps its own size, and a trim size can be typed
Three things the export and the page setup got wrong.
Spread mode made every sheet as wide as the widest so that one @page rule
would cover them all. That put a cover — one page — in the middle of a
spread-sized sheet with white either side, which is not a cover. Each sheet
is now sized to what is on it, and the print CSS carries a named page rule
for the single-page ones.
The trim fields clamped on every keystroke, so typing 500 meant the field saw
5, clamped it to the minimum and wrote that back under the cursor: the number
could not be typed at all, only nudged with the arrows. The field now keeps
what is being typed and tells the document about values it can use. The
arrows are unchanged, and leaving the field still clamps — someone who typed
5 and clicked away meant the smallest book.
And the autosave could loop. The save effect depended on the whole store
object, which is new on every render, so after a conflict — where the local
document is by definition not the saved one — it was a save attempt per
render: a 409, a re-render, another attempt. It depends on the callbacks now,
and a conflict holds the autosave until somebody answers it.
* feat(journey): page numbers pick their own colour
A book has dark pages and light ones, often on the same spread, so one folio
colour is wrong on half of them: the warm grey that reads on paper vanishes
into a full-bleed night photograph, and white vanishes the moment the page is
white. The way out used to be setting it per spread, which the document has
no place for.
The renderer asks what is under the number instead. It knows every element
and where it sits, so it walks them from the top down and takes the first one
that covers the spot: a shape's fill is arithmetic, a photograph gets white
with a hairline of shadow — unknowable without decoding it, and dark more
often than not once printed — and bare page takes the page colour.
A heuristic, deliberately: the alternative is sampling pixels from an image
that may not have loaded, in a renderer that also has to run for print.
Anyone it gets wrong can still pick a colour, and picking one turns it off.
* feat(journey): see who else is in the book, and where they are
Two people editing the same document without seeing each other is how you get
two people editing the same spread. So: a badge in the bar saying who has the
book open, and an arrow with a name on it for each of them.
The coordinates are millimetres on the spread, not pixels on a screen — the
two of you are at different zoom levels on different monitors, and a pixel
means nothing to the other one. They carry a spread index too, so a pointer
on page 40 is not drawn on page 2.
Ephemeral throughout. Nothing is written down, the room is the authorisation
for the pointer path, and a socket that dies takes its arrow with it rather
than leaving it on everyone else's page. Presence is keyed by socket because
two tabs are two pointers; the badge folds them back into one person, because
two tabs are not two people.
The rate limit needed room. Pointers move about ten times a second, three
times what the per-socket limit allows, and that limit is what stops a client
flooding the process — so it is now two: an outer ceiling every frame counts
against before parsing, and the original tight limit on everything a pointer
is not. Malformed JSON still counts against the tight one, which is what the
guard was for.
Throttled to ten frames a second going out and interpolated coming in: sixty
would be five times the traffic for something the eye cannot use, and drawing
ten as they land reads as a stutter rather than a hand.
* fix(journey): keep the zoom bar on screen at every zoom level
Zoomed far enough in, the bar slid off the bottom of the window — and it is
the only way back out, so the editor was stuck at 300%.
The workbench sat in a grid track sized `auto`, which grows to whatever is in
it; `overflow: auto` on the child does not stop that, because a grid item's
minimum size is its content unless told otherwise. So at 300% the box grew to
2400px inside a 660px window and took its absolutely positioned bottom edge
with it. The track is bound now, and the workbench scrolls instead of growing.
The bar also has a z-index, for the same reason it has to stay in view: it
must never be the thing something else covers.
* feat(journey): Studio replaces the old book export
The journey PDF and Studio were two ways to print the same book that disagreed
about what it looks like — and only one of them can be edited. The old export
goes: the two buttons on the desktop already sat next to the Studio one, and
the renderer they called is deleted rather than left as a second answer.
Nothing takes its place on the phone. The book is a Studio document now and
Studio is a desktop editor; a phone-sized PDF of something laid out for print
was the old export's answer to a question nobody was asking.
* feat(journey): lay the book out the way a travel book is laid out
The auto layout built every spread the same way: a picture on the left, words
on the right, and nothing else. It came out looking like a template because it
was one — and on a journey with little in it, like a stack of blank paper.
Five layouts now, chosen by what an entry has and alternating as the book goes
on. A place page for a stop nobody wrote about, with the country pale behind
the name and the coordinates under it. A story page with a small map of the
stop and the picture bleeding off three edges. A panorama across the fold. A
gallery with the hero on one page and a ruled strip on the other. And every
fourth picture spread goes full bleed with the words set over it, because a
book of loud pages has no loud pages.
The marks are what actually make these pages: a flag beside the country, a
date numeral, a chip saying which day this is, a rule along the foot with a
dot showing how far in you are. Each is trivial and together they are the
difference between a page that was designed and one that was filled in. All of
them are elements the panels can make, so what comes out can be taken apart.
The summary spread gained a heading and the furthest point drawn as two dots
and a line — that figure means nothing until it is a distance between two
names. The countries page went dark, because the printed one is: pale outlines
on near-black is the single most recognisable page in these books.
Two things the old layout got wrong are fixed on the way. Entries with nothing
on them share one page instead of taking a spread each, in the order they
happened rather than gathered at the end. And an entry whose photographs have
not arrived gets the frames they will go in, empty, rather than blank paper —
the layout is finished and the pictures are the only thing missing, which is
the right way round.
* feat(journey): build the spreads out of areas, and set the elements in ink
Two things, both about what a page is made of.
The layouts were photographs with captions. A printed travel book is built out
of areas: a page laid solid in ink with pale type on it, a band down the outer
edge, a picture that overruns the panel it sits against, a box ruled around a
block of text. Two more layouts do that now — one with a whole page in ink and
a second photograph crossing the fold onto it, one with an edge band and the
words in a ruled box, pictures at two sizes and one overlapping the hero. Six
layouts on a rotation, so the book stops reading as a template.
None of it needed a new feature. The document paints in array order, so a panel
pushed first is a panel everything else lands on; that is all layering is, and
it needed using rather than adding.
And the travel elements are ink by default rather than the app's orange. A book
is printed and a printed page is ink on paper; a colour that arrives without
being chosen turns every element into the same accent whether the book wants
one or not. Picking one is a click. The dark map is the exception — the route
goes pale there, because ink on a near-black country is a line nobody can see,
and only the default is overridden: a colour somebody chose stays theirs.
* fix(journey): three things the editor got wrong
An empty photo frame rendered on its own, ignoring frameStyle entirely — so a
Polaroid placeholder was a plain dashed rectangle until a photograph landed in
it and the chin appeared from nowhere. The two branches are folded together
now: the surround is drawn once and only what goes inside it differs, so a
frame shows what it will look like before it is filled.
Adding a page to a book with no inner pages yet put it in front of the cover,
where it could not be moved back from either — a move only ever swaps with
another inner spread, and the cover is not one. An empty book reports its last
inner spread as -1, and -1 + 1 is 0. The insert is bounded at both ends now.
And snapping only knew the outer safe lines. A spread draws that dashed rule
inset from each of its four vertical edges, so dragging inwards caught the fold
and nothing else — the line you could see just before it was not a target. All
four are, along with the centre of each page and the quarters within it: on a
spread the halfway mark is the fold, so a picture centred on its own page had
nothing to find.
* feat(journey): take the auto layout's spreads from templates drawn by hand
The layouts written as code are what a function can reason its way to: a
picture here, words there, margins that add up. What they are not is designed —
a panel bled off one edge, a shape running off a corner, a strip of pictures at
two sizes with the coordinates set beneath them. Those decisions come from
looking at a page, and the editor is where looking at a page happens.
So a spread built in Studio can now be the layout. `extract-templates.cjs`
reads a journey's book and writes its spreads out as data, with every
measurement divided by the page it was drawn on — a template built on a 210mm
square lays out on an A4 landscape book without being redrawn. `applyTemplate`
does the other half: it fills what the entry can answer for and copies
everything else as drawn.
That division is the whole contract. Text carrying a binding takes the title or
the story; empty frames take the photographs in order; the day, coordinate,
date and country marks take what the stop knows. Panels, rules and decorative
shapes are the design and are left exactly as they were put.
A template is one *for an entry* when it uses one — which has to come out of
the drawing rather than a label somebody remembers to set. A summary spread of
figures and country outlines reads perfectly well on its own, and dropping a
day's photographs into it would be nonsense.
Two templates ship with this, drawn by the person who will use them. The
built-in layouts stay as the fallback for entries no template suits.
* feat(journey): turn elements freely, and pass a spread on as a file
Rotation by the corner, the way it works everywhere else: a handle sits
diagonally outside each of the four corners of the selection, and dragging one
turns the element by the change in angle rather than to the angle under the
pointer, so it can be picked up anywhere without jumping. Held to 15 degree
steps unless shift is down. Four handles rather than one because a single
handle off the top right sits under the inspector for anything in the right
page's upper half.
And a spread can now leave the book it was made in. The zoom bar downloads the
one on screen as a file; Import, beside Add spread in the pages rail, brings one
back. Measurements travel as fractions of the page, so a spread drawn on a 210mm
square lays out correctly in an A4 landscape book, the same way the auto
layout's templates do.
What deliberately does not travel:
- Photographs. The file carries the design, and the frames arrive empty.
- The map source. A static map freezes the URL it was rendered from, and on a
Mapbox instance that URL has the access token in it, so a file meant to be
posted in a forum would carry a credential. Coming the other way, a tile
template from a stranger is a URL every reader's browser would fetch tile by
tile: an IP address handed to whoever wrote the file.
An imported file is treated as what it is, somebody else's JSON. It is read in
the tab that chose it and never uploaded, parsed and never evaluated, and every
element goes through the document contract, which drops unknown fields and
range-checks the rest. Colours in particular are #rrggbb or nothing. The size of
the file and the number of elements are both capped, because the damage a
hostile file can do here is not code execution, it is handing the editor more
than the server will accept and leaving a document that will not save.
Also fixes the rotation handle being drawn but not grabbable: the chrome layer
and the selection box both pass pointers through, and a handle has to opt back
in with pointer-events.
* docs(wiki): add two Studio screenshots for the addon page
The editor with a stats spread open, and the frames panel. Both are what the
Journey addon page will want once Studio ships, and both are wanted now to show
the feature off while it is still in review.
* test(realtime): cover the book room's refusals and its dead sockets
The realtime coverage ratchet caught what the book room work left behind: the
three handlers each have an early return for a message that is missing its
journey id or comes from a socket with no user, and none of them was exercised.
Every one of those is a silent failure by construction. A handler that did
something with a missing journey id would not throw, it would put a socket
somewhere it does not belong, and nobody would notice until two strangers were
looking at the same page.
The other half is sockets that are in a room but cannot be used: a connection
that dropped before its close handler ran, or one put in a room without ever
being registered. The peer list has to walk past both rather than showing a
ghost, and the broadcast has to skip them rather than throwing on a path that
runs ten times a second per editor.
Both files are back to full branch coverage, so the domain's pins hold as they
are rather than being lowered to fit.
* feat(journey): make the route map a map of the journey
Four things were wrong with it, and they compounded.
**It showed the wrong place.** The view was fitted to the country outlines as
well as the stops, so a trip that stayed inside Berlin was drawn as the whole of
Germany with two dots in the middle of it. It fits what the journey did now, and
the outlines run off the edge of the frame the way a coastline does on any real
map. Fitting to the country is still there as a deliberate choice, for the page
every travel book has, and how much air to leave around either is a setting
rather than a constant.
**It drew a line nobody travelled.** Stops joined in order is a claim about the
journey that is almost always false: it crosses mountains, water and borders the
trip went around. TREK already stores routed geometry per place, from the
planner and from imported GPX, so the map now follows the road where the trip
knows it and falls back to joining the stops where it does not. Segments stay
separate, so a trip with a flight in the middle does not grow a road across the
sea. The geometry is thinned when it is frozen into the element: a printed line
gains nothing from a point every fifteen metres, and a book carrying every GPS
fix of a three-week trip is a document nobody can save.
**The tiled and static sources drew nothing at all.** The tile grid was
computed, the route was projected onto it, and then no picture was ever
rendered — the map came out as a frame with a line across it. Three separate
faults behind that: the imagery layer did not exist, `{s}` was left unsubstituted
so every tile URL named a host that does not resolve, and the grid was cut from
the tiles the route happened to sit on rather than from what the frame shows, so
it stopped short of the edge. The frame decides the grid now, and it covers by
construction. Tiles overlap by a tenth of a millimetre, which closes the hairline
of white paper that rounding left between them.
Mapbox needed one more thing: `mapbox/standard` is a 3D GL style and the Static
Images API refuses it outright, so an instance configured for the planner's map
was configured with a style this cannot use. It falls back to the classic
equivalent, and a URL frozen into a document before that existed is repaired on
the way out rather than left broken for good.
**A map was always a rectangle.** It can be cut to the outline of the countries
now, so the picture *is* the country: no frame, no box, the page showing through
around the coastline. That is the version worth laying next to a photograph, and
the one thing a rectangle can never be. The tile grid and the vector fit are
projected from one extent so the picture and the shape it is cut to line up —
two fits meant the country was full of the wrong towns.
Also here: the credit line comes off the map itself. It is still resolved and
still stored, for the colophon, which is where a book has always credited its
sources.
* feat(journey): split the country list, and trim the map cards
The country list and the country grid were compositions somebody else made:
they decided the order, the spacing and the type size, and the only way to
change any of it was to not use them. They are gone, and what is left is the
same entry — the name over its silhouette, set the same way — as an element
placed one country at a time. Three down a page is the list, with the spacing
yours; one at 80mm is a chapter opener, which the list could never be. The name
is editable, so numbering them "1. Germany", "2. Netherlands" is typing rather
than a feature. The compact mark, outline beside the name at the size of a line
of type, stays for the places that want a mark rather than a heading.
The map section offers the four vector styles and nothing else. Tiles, Mapbox
and the cut-out are one click away in the inspector, where the rest of the map's
settings already live, and four cards that are the same map four times were
crowding out everything under them.
A map is fitted to the country by default now, which is the picture a travel
book usually wants. Fitting to the stops is still there and still matters the
moment the trip is smaller than the country it happened in.
Also: no navigation fallback from the dev service worker. In production it is
right — offline, a deep link has to be answered by the cached shell. In
development it answers a reload with the index.html it cached earlier, whose
script tags point at module URLs from before the last restart, so the browser
re-runs the previous version of the app while the editor shows the new one.
Every explanation for that is wrong until someone thinks of the service worker.
* fix(journey): drop what the trimmed map section left behind
Three bindings that only fed the cards removed in the commit before: the map
sources hook, the country codes the cut-out card needed, and the frame the
preview sized itself from. Lint had them as warnings rather than errors, so
they would have ridden along quietly.
|
||
|
|
7164f4fb11 | fix(photos): drop the dead initialiser eslint flagged in the exif reader | ||
|
|
503a03792e |
test(photos): cover the local EXIF path so the memories ratchet holds
The capture backfill landed in src/nest/memories/ with its local branch untested, which took the domain under its coverage floor (functions 96.71 vs 97, branches 79.67 vs 81). Raising the tests rather than lowering the gate: the path guard against climbing out of the uploads tree, a missing path, an unreadable file, the CreateDate fallback, empty EXIF, a throwing row lookup, and schedule(). Plus the Immich album mapping with and without coordinates. One note left in the test file for the next person: a mock that *rejects* makes vitest fail the run even when the code catches it, because the mock's recorded result is a second reference to that promise with nothing attached. A bare mockReset() is enough to trip it, so the beforeEach sets a benign default. |
||
|
|
2c333cb643 |
perf(journey): render the gallery in pages instead of all at once
A long trip's gallery is hundreds of tiles and every one of them was in the DOM from the first paint, which is what made scrolling it a chore (#1614). It now renders 60 and grows as the sentinel comes into view — the same mechanism the provider picker already uses, rather than a second one invented for this. Deliberately not pagination over the wire: the gallery arrives inside the one getJourneyFull payload that the client, the MCP tool, the MCP resource and the PDF export all read, so cutting it there would change the answer for four consumers at once and would silently shorten the book. The reported symptom was the scrolling, and that is client-side. The reported 500-photo limit is not in the code — no query limit, no upload cap, no client slice — so nothing was raised. |
||
|
|
06b7f3e690 |
feat(weather): answer for the hour when a past entry carries one
A journal entry made at 14:30 wants the weather of that hour. The archive path only ever asked for daily figures, so it answered with a rainy morning and a bright afternoon averaged into one number (#1614). With a time in hand it now asks for the hourly series as well and reads that hour, falling back to the daily figures when the hour has no reading — an archive gap must not turn into no answer at all. An unparseable time is treated as no time rather than as midnight, which would have been a confident wrong answer. The cache key carries the hour, so the two do not shadow each other. Nothing changes for callers that pass no time, which is every existing one. |
||
|
|
97f86dcf46 |
feat(journey): let quick capture take a note, not just a photo
The ask was "photos or quick notes" while travelling, and the capture sheet only took photos (#1614) — so a moment worth catching without a picture had no way in short of opening the full editor, which is exactly what capture mode exists to avoid. Two rows rather than the full sheet's three: this is a note, not the story. The story is written later, when the entry is finished. |
||
|
|
6e4fda897f |
refactor(journey): stop pulling trip photos into journeys
Photos live in journeys (#1614). Linking a trip used to copy its trip_photos rows into the journey gallery, which is how a photo one member had chosen not to share could reach a journey at all — PR #1967 filtered that at the door, and this removes the door. Nothing is lost by it: the trip-photo surface lost its UI in 3.1.0, so nothing writes to trip_photos on an install newer than that, and the client calls none of its routes. The table and those routes stay for one more release rather than being dropped in an append-only migration; the read paths keep working for installs that still hold rows. |
||
|
|
56b4509b03 |
feat(journey): put photos on the map where they were taken
The visible half of the capture-metadata work (#1614). Photos that know their coordinates are drawn as thumbnails on the Journey map, in the shape the report asked for: a picture rather than an abstract pin, and a count when several collapse into one. Clustering is a screen-space grid rather than a library. The Journey maps have never had clustering, and the one the planner uses hangs off react-leaflet while this map drives Leaflet directly; bucketing by rounded pixel position is a few lines, is deterministic, and does the job — photos of one place collapse into one thumbnail with a count, and pulling the map apart separates them again. The layer sits below the entry pins, because the itinerary is the point of the map and the photos are context. Its own effect, so photos arriving do not tear down and rebuild the map the way the entry effect does, and redrawn on zoom and pan because the buckets are in screen space. The thumbnail URL is built in the page rather than the hook: it needs the share token, which is the credential the byte proxy checks. Also re-points the trailing-migration guard at the new last slot, which is what its own comment asks of anyone appending one. |
||
|
|
11686be264 |
feat(journey): let a shared journey read newest-first
A journey shared while the trip is still running is read like a blog: the family following along wants the latest entry at the top, not to scroll past three weeks to find it (#1614). The owner sets it per share link, because it is a property of how the link is meant to be read rather than of the journey, and the reader can flip it either way. Off by default, so an already-published link does not reorder itself under its readers. Only the reading order changes. Day colours and stop numbers stay chronological, so the timeline still matches the map when the feed is upside down — which is why the day index is looked up rather than taken from the render loop. Reuses memories.newest/oldest, already translated in all 23 locales. |
||
|
|
e809808572 |
fix(journey): give the public map one numbering instead of two
The marker showed the stop's position within its day; the day heading showed the day number. Same colour, different meaning, and on mobile the heading is not rendered at all, so the number had no key — which is exactly what the report described: the same digits over and over in different colours (#1962). Both now count stops across the whole journey, so every number appears once, and the timeline carries the same number next to each geocoded entry. The day colour still groups the day. The badge sits outside the photo-count gate on purpose: an entry with a single photo puts its title and location in the image overlay, but the stop number has to be readable either way. |
||
|
|
97220b72f6 |
feat(journey): carry capture metadata into the payload, and keep it behind share_map
The gallery rows now hand the client taken_at, lat and lng, which is what the map and a capture-ordered gallery will read. The coordinates needed a projection before they could ship. A photo's capture location is a place the owner never typed and may not expect to publish, so it follows share_map — the same switch the entry coordinates already follow. Without that, turning the map off and leaving the gallery on would have handed out exactly the locations the owner had just withheld. The capture time is not a location and stays either way; a blog-style view sorts by it. |
||
|
|
d2cf694882 |
feat(photos): read the EXIF of an uploaded photo
Closes the third source of capture metadata (#1614). Provider photos are asked for, uploaded files carry their own answer — and until now nobody read it, so the most common case of all, a photo taken on the trip and uploaded straight into the journey, could never appear on the map. exifr rather than a heavier image library: pure JS, no native build, so no musl-pin dance in the Alpine image. Only four tags are picked, so a large file is not fully parsed. The resolved path is checked against the uploads root before it is opened. One thing worth knowing before someone files a bug about it: the client converts HEIC before upload and that conversion drops GPS, so an iPhone photo arrives here already stripped. Nothing to read is the expected outcome more often than not, which is why the empty case is silent. |
||
|
|
359c3afc21 |
feat(photos): ask the provider where and when, and stop the album path dropping it
Second half of the capture-metadata foundation (#1614). The album paths were the asymmetry: Immich's search has always carried exifInfo.latitude/longitude, but the album mapping quietly left them out, and the Synology album listing never asked for the gps block at all. So a photo picked out of an album could never be placed on a map, and the distance sort that PR #1632 added silently had nothing to sort by. Both now carry the pair. For the add itself the client only sends an asset id. Widening that contract would have meant trusting the client for something the provider can be asked for directly, and would have left the MCP path and every already-imported photo without it — so PhotoCaptureBackfillService asks afterwards instead, detached, skipping rows that already know both so an album import is not one provider call per photo. A slow or unreachable provider cannot hold up the add, and a photo without capture metadata stays the normal case. |
||
|
|
6185cf96b6 |
feat(photos): record when and where a photo was taken
The foundation the Journey map needs before photos can sit on it by their own coordinates, and the gallery before it can order by when a picture was taken rather than when it happened to be added (#1614). Three nullable columns on trek_photos plus a partial index over the coordinate pair; a photo with neither is the normal case, not a failure. recordCaptureMetadata merges rather than overwrites, for the same reason recordLocalThumbnail does: the sources disagree about how much they know. A provider search answers with coordinates, the same provider's album listing does not, and a local file gives them up only once its EXIF has been read. Whichever arrives first wins, and a later, emptier answer must not blank it. Coordinates are stored as a pair or not at all — half a pair is not a place, it is the null island. |
||
|
|
3e4b36ac71 |
fix(weather): give the Open-Meteo calls a deadline
All six outbound calls ran without an AbortSignal, so a hung connection held the caller until the socket gave up on its own. Every other outbound client in the codebase carries a timeout; this one is the reference domain and did not. |
||
|
|
85794a5a59 |
fix(journey): keep the picker's date headings chronological, and name any provider
Since the provider picker started sorting by distance to the entry, the date groups were built from the already-sorted list, so first-seen order put the day holding the nearest photo at the top and the headings stopped reading in order. Grouping now sorts its own output; the order within a day is untouched, so the distance sort still does its job where it belongs. The picker also called every non-Immich provider "Synology Photos". It now falls back to the provider's own id, the way the gallery view already did. |
||
|
|
edb487fbef |
feat(journey): let the desktop editor set the entry time, and reach the camera
The time was already stored, sent to the server and shown in the timeline, on the map, in the PDF and on the public share — the desktop editor just never rendered an input for it. So a draft carried the wall-clock time it was created at, it showed up everywhere, and there was no way to correct it (#1614). The column holds HH:MM:SS and a time input wants HH:MM, so both the initial value and the dirty check now trim. The camera gets its own hidden input rather than a capture attribute on the existing picker: putting it there would take the photo library away on a phone, which is the more common way in. The phone sheet already works this way. |
||
|
|
630e8efca1 |
fix(journey): stop unshared trip photos leaking into journeys, and gate the link
Two things found while reading the domain for discussion #1614. A photo a trip member left unshared is denied on the trip path — memories-access requires tp.shared = 1 — but linking that trip to a journey copied every row of trip_photos into the gallery regardless. From there it was readable by every journey contributor, and with an open gallery by anonymous visitors of the share link. journey_photos.shared cannot gate that after the fact: a direct upload writes 0 into the same column, so filtering on it would empty most galleries. The fix is at the door instead — only shared rows are copied in. addTripToJourney checked that the caller could reach the trip but never that they could reach the journey, so any logged-in user could link a trip of theirs into a stranger's journey and seed skeleton entries and photos there. The MCP tool has always checked this; only the REST route was open. Now the service checks it, which covers both and the internal call from createJourney. Neither path had a test. They do now. |
||
|
|
b267771772 |
fix(mobile): let the map show one day again
Collapsing a day card has hidden that day's places from the map since #216, and it was the only way to reduce a busy map to one day. The phone shell does not render DayPlanSidebar, so when it replaced the desktop page on phones that control disappeared with it: expandedDayIds stayed null and every day's pins sat on top of each other with nothing to do about it (#1962). A day chip in map view now drives the same declutter the desktop chevron drives, rather than introducing a second filter. Entering the map focuses the selected day, tapping another chip moves the focus with it, and leaving the map clears it so the timeline is never looking at a filtered state. Unplanned places stay visible, exactly as the collapse path has always left them. |
||
|
|
a3d1fc7808 |
feat(share): number the day's stops on the map and connect them in order
The public share page drew every geocoded place of the trip as an identical pin with no sequence and no line, so an itinerary could not be read off the map at all (#1962). It does filter to a day, but the only control for that sat below a 300px map, which is why the reporter never found it on a phone. Markers of the selected day now carry their position in that day's order, with the same contract the planner uses: a stop the day visits twice shows both, e.g. "1 · 3". A dashed line connects them in order. It is a straight connector on purpose, not a road route — drawing a real one would mean sending the itinerary to a third-party routing service for every anonymous visitor of a shared link, with no way for the trip's owner to opt out. A day picker now sits directly above the map, driving the same state as the day cards below, so the two can never disagree. Two things nobody reported, both found while in here. Outside a day selection every marker fell back to the placeholder indigo, because the payload nests the category on a day's assignments but sends it flat on the trip-wide pool and only the nested shape was read. And the map refit itself whenever the page re-rendered for an unrelated reason — the language picker, a late FX response — because the effect keyed on the array identity of a list the page rebuilds every render, throwing away the viewer's pan and zoom. It keys on the coordinates now. |
||
|
|
f4ee06e931 |
fix(share): stop the day header collapsing the title into a one-word column
The day card header on /shared/ is a single nowrap flex row, and its title block carried `flex: 1`. That is a flex base size of 0, so the moment an accommodation chip pushes the row past the available width the block freezes at its min-content width while the chip and the place count stay at full content width — the ~37px column with one word, or one CJK character, per line from #1955. It is content-driven, not viewport-driven, which is why it also showed up on a 1149px-wide desktop window: one long hotel name, or a day carrying both a check-out and a check-in, is enough. A breakpoint would have been the wrong fix. The title block now shrinks proportionally and truncates, the chip can shrink and truncates too, and the place count is pinned so it never wraps or gets pushed out of the card. The chip also moves off its hard-coded 9px onto the 10.5px the day sidebar uses in the app, with an icon to match — at 9px it was barely readable. |
||
|
|
28765e220b |
fix(places): keep the Google feature id, and give list imports the free address lookup
Two halves of #1954, both about a place arriving through a list coming out thinner than the same place pasted as a single link. The feature id was never dropped on purpose. googleFtidFromMapsUrl only looked at the `?ftid=` query parameter, while a share link that resolveGoogleMapsUrl has followed to its final hop carries the id in the `data=!…!1s0x…:0x…` path blob — the same run the coordinates are already parsed out of two lines further down. So every pasted maps.app.goo.gl link lost it, and the Maps button opened a name search rather than the entry the link pointed at. The parser now reads the blob too, but only on a /place/ URL: a /maps/dir/ route carries one !1s per waypoint and the first is the origin, which would attach the wrong place. List imports never had a reverse lookup at all. The only pass behind them is the Google enrichment, which returns immediately without a Maps key, so an instance without one imported a list and left every address empty forever. They now get the same free Nominatim lookup the single-link path has always had: detached after the import answers, on the throttled background lane so it never queues in front of someone's search, through COALESCE so it can only fill an empty column, and each row pushed over the websocket so the sidebar fills in without a reload. Capped at 250 places — past that it stops being a backfill and starts being bulk geocoding. Not included: an osm_id for imported places. Single links do not produce one either, and one derived from a zoom-18 reverse lookup would name the building or the street rather than the POI. |
||
|
|
c9278c02ce |
chore(deps): lift the four packages the Scout gate flags as high
The security scan on the release PR has been red on six HIGH CVEs, all of
them transitive production dependencies of the server workspace, which is
what ends up in the runtime image via `npm ci --workspace=server --omit=dev`:
brace-expansion 2.1.1 -> 2.1.4 CVE-2026-13149 / -69152 / -14257, ReDoS
reached through archiver -> glob -> minimatch
ip-address 10.2.0 -> 10.5.0 CVE-2026-69192, improper input validation
reached through the MCP SDK -> express-rate-limit
fast-uri 3.1.4 -> 3.1.5 CVE-2026-18446, reached through the MCP SDK -> ajv
js-yaml 4.3.0 -> gone GHSA-5p4m-2wfm-xmqj
Every one of them resolves inside a range the manifests already declare, so
this is a lockfile change and nothing else: no new dependency, no override,
no package.json edit.
js-yaml was the awkward one. @nestjs/swagger pins it exactly rather than by
range, and 11.4.5 pins the affected 4.3.0. 11.4.7 pins 5.3.0 instead, and
server already asks for ^11.4.5, so the affected copy leaves the production
tree without forcing anything. The root js-yaml 4.3.0 stays for eslint, which
never reaches the image.
Walking the production graph over the lockfile gives the same 482 packages
before and after, with only those four differing. Server suite is green
(409 files, 7805 tests), as are typecheck, typecheck:tests and client lint;
/api/docs-yaml still renders through swagger's new js-yaml.
Still open and deliberately not in here, both MEDIUM and below the gate's
critical/high filter: dompurify (via isomorphic-dompurify, and #1694 reverted
a bump of it once already) and hono (via the MCP SDK).
|
||
|
|
3011bf50b4 |
refactor(i18n): improve Turkish translations for vacay module
Replaces the machine-translated strings in the Vacay domain. The visible ones: 'Doygunluk' (saturation) and 'Güneş' (the star) were the Saturday and Sunday column headers, 'Sol' (left, as in direction) stood for the remaining days, 'Efsane' (myth) for the legend, and 'Sigortala' (insure) read "fuse" as an electrical fuse. Buttons move from the infinitive to the imperative the rest of the locale already uses. Rebased onto dev and resolved as a union: the branch carried the file as it stood on 25 July, which would have dropped the 35 keys dev has gained since from #1074, #1631, #1633 and #1637. Those keep their existing translations; only the 47 strings this change actually rewords differ. vacay.selectRegion keeps '(zorunlu)'. en says '(required)' and the Add button stays disabled until a region is picked, so '(isteğe bağlı)' would have sent Turkish users looking for a button that never enables. |