Repository navigation
Merge dev into feat/orm - #2588
Merged
Merged
Conversation
The planner keeps the place dialog mounted while it is closed, so its search block survived a closing. Clearing the field on close (#2465) left the result list under the empty field, together with the "Search Google instead" line offering that list's query again, and a pick from that list wrote its name, address and coordinates over whichever place the dialog was opened for next. Closing now drops the list, the suggestions, the Google line's query and the billing session, and cancels a pending autocomplete. A full search or a suggestion lookup that answers after the dialog closed is discarded instead of filling the next opening, and no longer ends the session a new opening may have started.
…queries, and Chinese hours
… lives on the service The translated hours were taken as soon as any segment parsed. A day the parser skipped, such as a weekend written 周六、周日, then had no period, and the client reads a day without a period as closed, so a shop open on Saturday showed Closed there. Text that did not parse went out as a one-line list, which the client reads as Monday and splits at the first colon, and which kept the enrichment from falling back to OpenStreetMap's hours. amapOpeningToOsm now also reads Amap's documented form (a colon after the days, remarks in brackets with their own semicolons), 每天, 周末, 工作日, 星期, 到, 、, full-width colons, tilde ranges, 次日, 全天 and 24小时, and a day named only as closed. It returns null unless every weekly segment reads, and no hours are sent then. The autocomplete tips move from a module-level map into AmapTipStash, one instance on MapsService: a provider is built per request, and the pick and its details lookup are two requests. Only the tips actually served are kept, and a stashed tip without a coordinate answers null, so the client falls back to its text search instead of receiving a place it cannot pin.
…d a choice is saved once it holds The picker of other ways asked OSRM with nothing avoided while the rail drives a trip that avoids a road class through Valhalla, so the list had no current road, called a road the rail was not on the fastest, and choosing it changed nothing. A choice was stored as one via where the offer strayed furthest, without asking whether the router then drove it: a point on a ferry was pulled to the pier and the drive went overland, and a way weighed away from motorways kept the motorway after its one point. An OSRM answer standing in for a Valhalla that did not respond was cached as the avoided road, unmarked. The rail now hands out a router per leg with the leg's own mode, avoided classes and engine, and its days carry the line of every leg. Offers come from that engine, and the rail's own leg always heads the list as Current. A choice is pinned and proven against that router with at most three pins before anything is written; a way it will not follow is not saved and says why, with a hint when it crosses by ferry, and a leg that changed meanwhile or a device offline writes nothing. The OSRM stand-in is filed under the plain key only and flags the day as not avoided.
The drive from where one day ended to where the next begins was the one stretch of a connected road trip that offered no other ways. The band above a card's first stop had no button on the desk, and the phone did not show that drive at all. A via behind a day's last stop, where the map already files a point dropped on that drive, was deleted when the day was reordered under Days, and was left on no stop when that last stop was removed or dragged up the day. Switching the avoided road classes or the day's profile kept the old road and minutes on it until a reload. The drive in now opens the same picker as any leg, on the desk band and on a new row at the head of the phone's chain that names where it leaves. A choice is proven with the seam's own router and written behind the last stop of the day before, also when a day with a single stop lies between. The rule for that via lives once in shared (carriedSeam): it stays with the last stop while that stop stays last, and is read like any other once it is not, for the planner's drags and removals as for the server's sorts and seated nights. The cached answer for a seam now records its mode and avoided classes, so changing either asks for it again.
…ys so A ferry between two stops without a clock was seated by the clock alone, which files a ride with nothing timed before it at the end of the day. The desktop day list stored that slot, and the road trip drove overland to the far shore and back to the pier before the crossing. A ride that leaves and lands on the same day, with both terminals located, now takes the seat between the clocks that adds the fewest straight-line kilometres. One rule in shared seats it for the day list, the slot the desktop stores, the phone and PDF lists and the road trip in the browser and on the server. Slots already stored are left alone. A flight, train, ferry, cruise or bus on no day made no seam and said nothing, while the map still drew its arc. The rail and the phone tab now name such a booking with a way to open it, and calculate_roadtrip lists it under undatedRides.
…d say why a way was not taken The slot the desktop stores for a ride within one day was worked out over the rows the day list shows, so a hotel it hides on the far shore was left out, the ferry was filed behind it and the road trip drove there overland before the crossing. It is now worked out over every stop the drive reads, timed the way the drive times it. An overnight ride no longer seats its arrival by the slot seeded on the day it left, and opens the day it lands on unless somebody placed it there. A via behind a day's last stop goes once that stop is no longer last, for the rail's drags and removals, the list's reorder and the server's sorts and seated nights alike; some of them used to bend a leg of the day through it instead. A leg OSRM drew while the avoidance router did not answer is now known as such: the picker no longer reads it as that router's road, has the leg routed again when its own road is chosen and says so, and a join that fell back is asked for again and flagged on its card. A refused way that leaves a class out names the setting that drives it, the ferry hint covers a crossing that lands the next day, a road another engine timed names that engine, and the desk bar keeps the keyboard on the chip while a choice is checked and announces the outcome. A ride on none of the trip's days is no longer said to have no date.
…d a refusal is said once A via cuts a drive into one routed leg per waypoint pair. Folding them back onto the stop pair summed the metres and seconds but kept the first piece's printed texts and end point, and the phone reads the texts first: a drive into the next day of 3.9 mi in 14 min showed 1.9 mi in 6 min. Every choice under "Other ways" writes vias, so every choice ran into it. The pieces are now merged by one shared helper that writes the texts from the sums in the reader's unit, for a day's own run, for the drive between two days and in the server's plan; the drive between two days is asked for again when the unit changes, as a day's run already is. On the desk a refused way was said in the bar's status line and again in a toast laid over that line. The desk keeps the status line; the phone, whose bar has no room for the sentence, keeps the toast.
…ne suites do The desk helper waits for road trip mode, which never turns on at phone width, so the case timed out before it asked anything. It now waits for the drive tab, as the phone feed suites already do.
…omes without them A leg a routing provider answers without its from and to points threw in the merge, and the day it belonged to never finished routing. Such a leg now keeps the first piece's ends; the texts are still written from the sums.
Signed-off-by: hiro-nikaitou <vieteviete@proton.me>
The trip's places went into the field's search results as they were. A place without a pin was picked as 0,0, because Number(null) is a finite 0, and a highlight left over from a longer search could point past the shorter list and throw on Enter. The phone's form, the train stations and the car stops were not offered the places at all. The places are now prepared once per form (named, with a real coordinate, each once) and handed to every location field of the manual tab, on the desktop and on the phone. The field shows them while it is empty or holds fewer than three characters, narrowed by what was typed, and derives that list rather than writing it into the search results, so a late search answer cannot replace it and a place already picked does not reopen it.
Which stay a day wakes up in and which one it sleeps in was decided in the client's Days view only. The rule, with the day order, the stay range and the check whether the hotel is the edge stop itself, now lives in shared under day/stay-bookends, typed on the few fields it reads, so the road trip in the browser and on the server can make the same choice as Days. The Days helpers re-export it unchanged, and their tests pass untouched; nothing behaves differently.
The drive learns what a booked night at the edge of a day is, while nothing seats one yet. One rule in shared, seatNightBookends, picks the stay a day wakes up in and the one it sleeps in the way Days does, over real nights and in id order, and puts each hotel at the edge of the day as a stop of its own that belongs to no assignment. The schedule, the day window, the spill into the next morning, the sums and the connected days read a night spent at one hotel as a leg going nowhere, never cut or postpone the drive to tonight's hotel, and pass the hotel over wherever a stop is looked up by its stored index. That lookup now also puts a place added behind a terminal where it fell. The rail and the phone chain draw the hotel as a row of its own that opens the booking or the stay, offers no other ways and takes no via, in every language. Until a trip switches it on, every plan is the one stored.
…before one ends there A trip can now switch on roadtrip_hotel_bookends, a shared driving preference that is off until somebody sets it, so every trip drives exactly as before. Switched on, the planner in the browser and calculate_roadtrip on the server seat the hotel slept in at the start of the next day and tonight's hotel at the end of the day, both through seatNightBookends over the trip's stays in id order, with the earliest booking linked to each. A linked booking changes the hotel row and asks the router nothing. The switch sits in the desktop driving settings under the route line, and the phone's driving figures carry the same switch as the one control they offer; both write through the planner's own preference save, so a flip offline queues like any other. get_roadtrip_context reports the stays, calculate_roadtrip previews the switch through its settings, and the settings, corridor and day boundary tools say what it does and that missing means off.
The road trip page gains a section on the new switch under the route line: off by default, shared by the trip, and on the phone the one control in the driving figures. It says how the hotel rows read, what they count towards, what a click opens, when a side of the day gets none, and that the drive to and from the stay takes no via and no other way. The daily travel times, the settings table, the phone section, the MCP table and troubleshooting point to it. The accommodation and planner overview pages name the switch and that it leaves Days alone, and the MCP tool page describes the bookend stops of calculate_roadtrip, the stays of get_roadtrip_context, the corridor and the ignored boundary over a booked night.
…s right A hotel row now takes its position and name from the trip's place instead of the stay row, which the planner fetches again only when a stay is edited. A moved pin no longer seats the hotel at its old spot while the server plans from the new one. The drive out of the morning hotel goes the way the first place is reached from it, the mode Days draws that leg in, and it is set in the shared rule so the browser and calculate_roadtrip agree. On the rail, a hotel row that took in the morning marker starts the line. A day that only drives from one stay to the next shows no stop count and takes a dropped stop as its first. The hotel keeps its pin on the desktop map, and the switch explains itself under its label and to screen readers. On the phone, the note about figures set on the desktop now sits above the switch it does not cover. The hint and the refusal on the hotel's drive say what they mean in all 23 languages. The via tools and the wiki say that a via into a booked night is kept but not used, the wiki states the overlap rule Days applies, and the arrived-from branch of the via anchor has tests.
…takes its via The check-out row showed the room's last hour and, beside it, the time the plan leaves the hotel, which can be hours later when a place further on is pinned. The row now says "Leaves after check-out" when it does, on the desktop and on the phone. A click on a road the day drives twice, out of the hotel in the morning and again between two of its own places, went to the first pass, the drive out of the hotel, and was refused. The second pass now takes the via, and where yesterday's drive into the hotel and today's leg share a road, today's leg does. A road only the hotel's drives use still takes none. The wiki says both.
…e, and a failed import does not look like a success Gemini behind the OpenAI-compatible provider decodes against the JSON schema TREK sends and fills declared properties only. reservationFor was an open object without any, so the model left it out: a Booking.com stay came back with its dates and price but no hotel name, and the mapper had to drop it (#2477). It is the same cause as #1638, whose fix only made the JSON readable. The schema now declares every field the prompt names inside reservationFor, for venues, flights, trains, buses, boats, events and rental cars, at most two levels deep, and requires it; the car desks at the root get the same venue fields. Every object stays open to keys the prompt does not name. On the cloud AI path an answer is cleaned up before it is mapped. Exact duplicate nodes are dropped, so one stay is one booking. priceCurrency is read as an ISO 4217 code ("EURials" is EUR, a lone symbol maps to its code); when the model names none, a priced node takes the currency of the document's total, and otherwise the field goes instead of saving a code nobody can convert. The kitinerary path is untouched. The import upload decodes file names as UTF-8 like every other upload route. "Bestätigung.pdf" reached the warnings as mojibake, and the review could not attach the file to the booking it created, because it matches on the name. The OpenAI-compatible client logs which response_format the answering attempt carried, metadata only, so the next report shows whether the schema reached the model at all. The background import card showed a green tick beside "no reservations could be extracted". A parse that found nothing now wears the warning mark instead; it is the same card on the desktop and on the phone.
…, as the other providers can Synology Photos, AirTrail and Dawarich each have a switch to trust a self-signed certificate; Immich had none, so every request to such a server failed with a bare "fetch failed" (#2475). Immich gets the same switch, per user and off by default: - users.immich_allow_insecure_tls (migration 243, column guarded) and a settings row labelled with the existing skipSSLVerification string, so no new translations. The settings card on desktop and phone draws it from that row. - Every request to the user's Immich (status, browse, search, asset info and proxy, albums, upload) passes rejectUnauthorized from the switch. Only 1 counts as on. A shared photo follows its owner's switch, since it goes to the owner's server with the owner's key. - Settings PUT and the connection test take allow_insecure_tls as a boolean. Absent on a save keeps the stored choice, so an older client cannot clear it; disconnecting turns it off again. - A failed request names its cause, e.g. "fetch failed (self-signed certificate)", instead of only the undici message. - The upload mirror gets a timeout: the journey upload waits on it. Fresh installs also never showed "Mirror journey photos to Immich on upload": migration 112 added that row only where the Immich provider already existed, and on a new database the seeds create it after the migrations. The row is now in the seeds, and migration 243 adds it where the provider exists without it. Wiki: Photo-Providers.md documents the switch.
… to the next A day that checks out of one hotel and into another, with nothing else planned, falls back to a drive from the first hotel to the second (#1297). A flight, train, ferry or coach booked on that day is the move itself, but one saved without its stations leaves no waypoint behind, so the day map, the trip overview and the phone plan all drew the full road between the two hotels, and the phone showed it above and below the flight (#2476). With its stations the day was already right. - The fallback drive between the two hotels is no longer drawn when any such booking is on the day, located or not. A day without a booking keeps it. No line beats a wrong one. - The phone plan takes the evening hotel leg from the last drive that reaches the hotel, not the first, so an earlier drive to the same spot no longer stands in for the one that closes the day. - Google Maps and CoMaps exports of such a day, which could only ever describe that same drive, return nothing, and on the desktop and the phone the two buttons are left out whenever fewer than two stops remain, instead of doing nothing on a tap. A hotel stop planned by hand ahead of the flight already sorts behind it through the ride seating of #2474; a test now pins that for the days view as well.
… sent The share dialog stored the pick as String(value) while its options are keyed by numeric user ids. CustomSelect looked the pick up with a strict comparison, found nothing and left the trigger on its placeholder, although the Invite button was already enabled and the invite went through. The phone opens the same dialog from the Mehr sheet, so it showed the same. CustomSelect now compares the held value and the option values as text, so a caller holding "5" finds the option keyed 5 and the other way round. A held null or undefined still matches nothing. onChange keeps handing back the option value unchanged. No other caller mixes the two kinds today; the place form had worked around the same trap by keying its options as strings, and its comment now says why the string stays. Tests: CustomSelect matches across both kinds and marks the pick in the list, the desktop dialog shows the picked name and clears after the invite, and the phone share sheet does the same through the real dialog. Refs #2478
…editable like in a trip A saved place always had price, currency, website and phone columns. Saving from a trip, the file import and the copy into a trip all carried them, but neither editor showed them and the update contract did not know them, so the validation pipe stripped them from a PATCH and the MCP tool refused them as unknown keys (#2471). - shared: collectionPlaceUpdateRequestSchema takes price (not negative, nullable), currency (three letters, trimmed and upper-cased), website (http or https, as on save) and phone (trimmed, capped at 60 like the file import). The save schema is unchanged. - server: updatePlace writes the four fields inside the existing transaction. update_collection_place spreads the same shape, so REST and MCP take and refuse the same values; its description now names the price and asks for the currency with it. - client: the desktop detail sheet and the phone sheet edit price with a currency picker, website and phone, and show them in read mode (price only above zero, phone as a call link, website as a link chip through safeHttpUrl). Both shells share one hook, useCollectionPlaceExtras, which sends only the fields the user changed and always pairs a price with its currency, since a list has no base currency. An amount that does not parse marks the field and holds the save. - i18n: collections.price, collections.priceHint and collections.phone in all 23 locales; the website label reuses places.formWebsite. - wiki: Collections and MCP-Addon-Tools name the new fields. Tests: schema spec, COLLECTIONS-SVC-128 to 131, controller pipe parity, update_collection_place with the new fields, COLLECTIONS-E2E-085 to 087, FE-COMP-COLEXTRAS-001 to 012, FE-COMP-COLDETAIL-049 to 054 and FE-MOB-CPLSH-042 to 046.
…import fixes Saved place price (#2471) - A price above zero that would be left without a currency is refused with a 400, over REST and through update_collection_place alike. A list has no base currency, so such an amount was read in each viewer's own default currency and went into a trip as the trip's. A price sent alone keeps the stored currency, and a zero price or clearing both fields still goes through. The check sits in the service because the MCP tool spreads the contract's shape, where a refine would never arrive. - An older row that holds an amount without a currency shows it as a bare number instead of in the viewer's own currency. - The Hungarian hint uses the informal register of its file, which is now prettier clean. Immich certificate switch (#2475) - The switch trusts one server. Saving a different URL without sending it turns it off; saving the same URL keeps it as before. The wiki says so. Moving day (#2476) - Google Maps and CoMaps are left out only when there is nothing to hand over. A day with a single stop opens it as a pin again, on the desktop and on the phone, as it did before. - Desktop: a moving day whose flight or train has no stations shows no route tools, since there is nothing left to draw. With stations they stay. - Phone: the drive between two stays on a day without stops or bookings shows once at the top instead of again at the bottom. - The export helpers take the carrier flags as one object instead of two booleans in a row. Import card (#2477) - The success and error marks use the theme tokens. - The e2e test names its upload with a made-up booking number. Tests: COLLECTIONS-SVC-132 and 133, COLLECTIONS-E2E-088, update_collection_place with a price and no currency, FE-COMP-COLEXTRAS-013 (011 adjusted), IMMICH-TLS-010, IMMICH-103 extended, FE-PLANNER-DAYPLAN-222 adjusted and 224, FE-MOB-PLTL-051, FE-MOB-PTLM-047, FE-W4BGT-023 and 024 check the mark colours.
…cked A connection test against a server with a self-signed certificate ended in "fetch failed (self-signed certificate; if the root CA is installed locally, try running Node.js with --use-system-ca)". The advice is meant for whoever runs the server and cannot be acted on from the settings card, so it is cut off. The save button of a collection place stays locked while the price cannot be read, but looked and pointed like an active one. It is dimmed and shows the not-allowed cursor now.
… again Takes back the editable price, currency, website and phone on collection places from 0df8218 and its review follow-up in a910c2a. Costs are kept inside a trip only; a collection does not carry them, and #2471 is a feature request rather than a bug. The collection files are back to how they were before, including the dimmed save button from f16698b, which only mattered for the price field.
…tops are hidden in Days Since 4.3.0 every lodging booking types its place as 'hotel', and 'hotel' is one of the road trip stop types. With "Show in Days too" switched off, Days dropped every such place from the trip's place list, so a hotel vanished from the sidebar, from the booking's own place picker and from the Days map the moment it was booked, and a click on it in the day list found nothing. Nothing was deleted: road trip mode kept showing it. Days now leaves out only road trip stops nobody booked. A lodging place stays, and so does any place a stay or a booking points at, on the desktop and on the phone. Trips planned on 4.3.1 show their hotels again without any change to their data. An imported GPX or KML track is no longer offered as the place of a stay, in the booking form, the day's hotel picker and the phone's sheets. A stay that already names a track keeps showing it.
With the Unplanned filter on, a click on a stop in the day list moved the map while a click on the stay chip did nothing: the map only went to a selected place it showed as a pin, and the GL map also to one of the open day's stops, which a stay is neither. Both renderers now look the target up the same way, the pin first, then the day's stop, then the selected place itself, so the map goes to the stay whatever filter hides its pin.
A place from the TREK index can carry its website the way Overture has it, such as fr.wikipedia.org/wiki/Chapelle_Sainte-Barbe_du_Faouët, and saving it to a collection or a trip failed with "website must be an http or https URL" over a field the dialog does not show (#2483). OpenStreetMap, Amap and booking mails hand over websites the same way. Every place a website comes in from outside now goes through one shared helper, normalizePlaceWebsite: the index client and its nearby search, the Overpass and OSM detail mappers, Google search and details, Amap, plugin search and the booking import. It completes a bare or protocol-relative host to https, keeps http and https as they are, and drops every other scheme and anything that is no address. Only the scheme is added, so an accented path is stored as written. placeWebsiteSchema completes a bare host the same way before its check, so an older client, a cached offline result, an MCP agent or a plugin is no longer refused, while javascript:, data: and mailto: still are. The REST controller, the plugin RPC and the enrichment now store the parsed value instead of the raw one, and create_place_accommodation takes the website through the same schema as create_place. safeHttpUrl still links only a value already stored in its final form, so nothing changes on screen. Tests: SHARED-WEBSITE-001 to 012, SHARED-PLACE-030 to 034, SHARED-COLLFILE-2483-01, COLLECTIONS-E2E-089 and 090, PLACES-E2E-2483-01 and 02, PLACES-CTRL-2483-01 (the louvre.fr case adjusted), MCP-PLACES-2483-01 to 03, MCP-ACCOM-2483-01, MCP-COLL-2483-01, PLACES-RPC-2483-01, MAPS-2483-01 to 05, MAPS-POIS-015, MAPS-GERS-010, AMAP-012b, TREK-PLACES-2483-01, PLUGIN-SEARCH-2483-01, BOOKING-IMPORT-2483-01, ENRICH-052e, ENRICH-102 extended, ENRICH-120 adjusted, FE-UTIL-SAFEURL-007.
…navigation without a second heading; white admin page
…About, scope picker in the new look
…user defaults and notifications get pages of their own
…e release's new tasks, texts and translations to match
- Make the “Manage preferences” text in notification emails clickable. - Link directly to `/settings?tab=notifications`. - Preserve the localized link text and existing email styling. - Add a regression test for the generated preferences URL.
…t line breaks overflow-wrap:anywhere alone lets a long URL wrap inside a flex row; break-words is overridden by it and word-break:break-word is deprecated. MarkdownText already gets the rule from .collab-note-md, so its call sites drop the duplicate.
…t passes on Sundays
The minimum-rating filter lived in the desktop sidebar's local state, so it only ever narrowed the desktop list: the map markers and the phone's places list ignored it. It now sits in the trip store beside the pool and category filters, and one module (utils/placesFilter) holds the pool, category and rating matchers that the desktop list, the phone list and mapPlaces all use, instead of three hand-copied versions of the same checks.
On a phone the only way to narrow the trip's pins was to leave the map for the places list, and the rating floor could not be set at all. The phone map now has a round "Filters" control at the foot of its right-hand stack, badged with how many filters are on, opening a sheet with the pool (all / unplanned / planned / tracks), the minimum rating (the desktop list's floors), the trip's categories including "no category", and a reset. It is hidden on the road trip stage, which draws its own pins. The places browser's filter panel gains the same rating floor; its category rows move into MPlacesFilterControls so the panel and the sheet share one copy. Everything writes the trip store, so list and map stay in step. MapTogglePill takes an optional count badge.
The sheet opens over the phone map, and with the default bar glass the map's place names showed through the pool chips, the rating floors and the category rows, which made them hard to read at 390px wide. It now uses the opaque sheet material, like the other menus that open over busy content.
Map-Features gets a "Filtering the places on the map" section: the markers follow the places list's Show, category and rating filters, and on a phone the Filters button of the plan map opens the same choices in a sheet. Places-and-Search says the rating floor narrows the map markers too.
…er hook The fallback from "Tracks" to "All" once the last track disappears lived in the desktop sidebar and the phone places browser, neither of which is mounted while the phone map is in front: the map could sit empty under a filter no control offers any more. useTripPlanner now owns it, once, for every screen.
MapTogglePill always set aria-pressed, so the phone map's Filters control read as a toggle although it opens a sheet. An opensDialog option drops aria-pressed for aria-haspopup="dialog"; the other map toggles are unchanged.
MChip only showed its state through colour. A pressable option renders aria-pressed from active; the places filter's Show and rating floor chips use it, and the tests now check the chosen chip by its pressed state.
The header claimed the lists and the map always agree. They apply the same predicate to the same values, but the map narrows "planned" to the open day (#2024) and leaves collapsed days' stops off.
…trol The panel button is named "Filters" like the map control, its badge counts kinds of filter through countActivePlacesFilters (the pool, shown as chips above it, stays out), and picking a rating floor clears the selection as picking a pool does.
…ory matcher PlacesSidebarHeader uses the UNCATEGORIZED constant, placesBrowserModel stops re-exporting matchesCategoryFilter (its test imports it from utils/placesFilter), and the orphan comment in the en places strings joins the places.filters key.
Brings the ORM branch up to date with dev (270 commits since bf0507d). The ORM architecture stays as it is; dev's changes are ported onto it: legacy steps 243-258 become MikroORM migrations carrying their "Legacy migration step N" markers, new raw-SQL services move onto repositories, and dev's tests run on the ORM harness.
…cide's icon renames feat/orm raised react to ^19.3.0 and several plugin-sdk dependencies without regenerating the lockfiles, so npm ci failed on every CI job. The root overrides now pin react 19.3.0, both lockfiles are regenerated with npm 11, and the tests that select lucide icons by their old class names use the current ones. Two tests cover the Immich prefs reads and the boot restore branch that the coverage thresholds were missing.
…run mode on CI With CI=true typescript-eslint infers single-run mode, builds the program from the fixture file on disk and ignores the code each case passes in, so all 24 invalid cases reported nothing on the runner. Both testers now set disallowAutomaticSingleRunInference.
… of their purpose, like the rest
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merges current
devintofeat/orm. Your architecture is untouched; only dev's behaviour sincebf0507ddbis ported onto it.Review:
git show --remerge-diff d7e65623fshows only the conflict resolutions (the +289k is dev's own work since the branch point, mostly help center and i18n). The four follow-up commits are small:git log -p d7e65623f..orm-sync-dev. Merge as a merge commit, not rebase.What changed
Legacy migration step Nmarkers and your naming, so installs at any legacy step baseline and boot.uow.transactional, detached work openswithRequestContext.Fixed on feat/orm itself: lockfiles out of sync after the react 19.3 and plugin-sdk bumps (
npm cifailed), lucide 0.577 class names in tests,eslint .out of heap on the runner,no-promise-as-value.test.tsfailing underCI=true.Please confirm
LEGACY_SCHEMA_VERSION241 to 258 (it was already one step behind).tsxis a devDependency here, which breaks key rotation in the image (--omit=dev).src/db/repositories/**coverage pin: 99% branches, 89.83% measured on CI. It already fails on feat/orm (never measured there in CI). This is the only red item in CI; I did not lower the pin.reseat-booked-nights.tsnow carries dev's body and the SQL that dev had put insrc/nest.The other smaller decisions are in the details below.
Verified: typecheck, lint, server/client/e2e suites; every legacy step 242-258 plus a production snapshot (244) booted twice, no refusals or replays, schema identical to dev, production row counts unchanged. CI is dispatched on the branch because
test.ymlonly triggers on PRs to dev/main.Full report: conflicts, schema table, per-domain port, all 17 decisions, verification numbers, pre-existing issues
Summary
This merges
origin/dev(b6115d4) intofeat/orm(378b157) and ports every server behaviour dev gained since the branch point bf0507d onto the ORM architecture, which itself is unchanged: noDatabaseService, nodbproxy, noservices/, no raw SQL insrc/nest. Dev's 16 new positional migration steps (243-258) arrive as 16 MikroORM migrations withLegacy migration step Nmarkers, so installs at any legacy step from 242 to 258 baseline and boot. Four follow-up commits fix breaks that already exist onfeat/ormitself (lockfiles out of sync with the manifests, lucide 0.577 class names in tests, the server lint step running out of heap on the runner, the rule tester underCI=true), add two coverage tests, and give the new migrations your naming scheme.Commits on top of
origin/feat/orm:d7e65623fMerge origin/dev into feat/orm (parents 378b157 and b6115d4, signed)af533fccefix(deps): sync the lockfiles with the bumped manifests and follow lucide's icon renames (signed)b653167a2ci: give the server lint step an 8 GB heap for the type-aware rules (signed)342d6edd3test(server): keep the no-promise-as-value rule tester out of single-run mode on CI (signed)35a7e3d08refactor(server): name the new migrations after the first seven words of their purpose, like the rest (signed)How to review
Start with the merge resolution only:
--remerge-diffshows what changed relative to git's own automatic merge: 325 paths (298 modified, 21 added, 6 deleted), all underserver/apart frompackage-lock.jsonand one wiki page. Of those, 142 were real conflicts (137 content, 5 modify/delete, recomputed withgit merge-tree 378b15716 b6115d48f); the rest are auto-merged dev files that still spokeDatabaseServiceor sync calls and had to be translated.Kinds of conflict and how each was resolved:
db/migrations.ts,db/seeds.ts,tests/unit/db/migration-hygiene.test.ts,nest/photos/trek-photos.repository.tsand its test. The deletions stand. Dev's changes went into the successors: the 16 migrations andPhotoProviderSeeder(two Immichphoto_provider_fieldsrows),TrekPhotosRepository.patchCaptureMetadataplusTrekPhotoRegistrationService.recordCaptureMetadata(now returns whether the row learned anything, test TREKPHOTO-006). Dev's only hygiene-test change was an allow-list comment, so it has no successor.uow.transactional.@Optional()):GoogleQuotaService(MapsService, GoogleTransitProvider),SettingsService(TripsService),PushSubscriptionsRepository(AuthService, AdminService),MikroORM(JourneyPhotoCaptureService, AtlasService). New entities are registered in the owning modules'MikroOrmModule.forFeature.LlmParseService.readsImages/readReceiptdid not awaitllmConfig.resolve(always answered "yes" / passed a Promise as config), andMcpResourceDiscoveryController.protectedResourcedid not await (floating promise).createTestMikroOrmModule). Dev's tests were ported with ids and assertions unchanged, made async where the service is. Where a dev id collided with one of yours, only the dev side was renumbered (see Decisions).--package-lock-only --ignore-scripts), keeping the 34libcentries and the musl pins.@mikro-orm/sqlite7.2.1 already depends on better-sqlite3 13.0.3, so the bump leaves a single copy.wiki/Real-Time-Collaboration.mdkeeps both texts.Auto-merged dev files that needed translation without a conflict: the whole google-quota domain, receipt-scan, Web Push subscriptions,
roadtrip-plan.service.ts,backup-archive.ts, and several dev tests that still builtnew DatabaseService(...)or rancreateTables/runMigrations(booking-import, receipt-scan e2e, oidc-local-provider).Schema
dev appended steps 243-258 to the positional runner. Each is now one migration after
Migration20200101040300(step 242), with exactly one marker. Column steps useaddColumnIfMissing; table and trigger steps useIF NOT EXISTS, so every step survives a replay.users.immich_allow_insecure_tls INTEGER NOT NULL DEFAULT 0; two Immichphoto_provider_fieldsrows, inserted only where the Immich provider row exists (also inPhotoProviderSeeder)Migration20200101040400_immich_learns_the_switch_synology_airtrail_and.tstrg_place_regions_follow_place(#2527)Migration20200101040500_a_place_that_moves_takes_its_atlas.tspush_subscriptionsplusidx_push_subscriptions_user(Web Push, #894)Migration20200101040600_web_push_one_row_per_browser_a.tsMigration20200101040700_the_2527_trigger_once_more_for_an.tsday_assignments.route_excluded INTEGER NOT NULL DEFAULT 0Migration20200101040800_a_stop_kept_on_the_day_but.tspacking_items.packed_quantity INTEGERMigration20200101040900_how_many_of_an_item_are_packed.tsbucket_list.region_code TEXTMigration20200101041000_a_bucket_list_wish_for_one_state.tsvacay_company_holidays.fraction REAL NOT NULL DEFAULT 1Migration20200101041100_half_company_holidays_0_5_covers_the.tsjourney_entries.is_draft INTEGER NOT NULL DEFAULT 0Migration20200101041200_a_journey_entry_still_being_written.tsshare_tokens.share_travel_only,share_tokens.share_hide_images(bothINTEGER NOT NULL DEFAULT 0)Migration20200101041300_two_narrower_ways_to_share_a_trip.tsbudget_settlements.note TEXTMigration20200101041400_a_note_on_a_settle_up_payment.tsgoogle_api_usage (day TEXT PRIMARY KEY, calls INTEGER NOT NULL DEFAULT 0)Migration20200101041500_google_api_calls_per_utc_day.tspacking_template_items.weight_grams INTEGER,quantity INTEGER NOT NULL DEFAULT 1,bag_name TEXTMigration20200101041600_packing_templates_remember_what_an_item_weighs.tsjourneys.status_override TEXTMigration20200101041700_a_journey_s_state_as_its_owner.tsplaces.email TEXT,places.opening_hours TEXTMigration20200101041800_a_place_s_e_mail_and_its.tsjourneys.photo_location INTEGER NOT NULL DEFAULT 0Migration20200101041900_a_journey_that_puts_an_entry_on.tsSteps 244 and 246 share one helper,
createPlaceRegionsFollowPlaceTriggerinmigration-utils.ts. The folder now holds 260 migrations, 258 of them with a marker (the baseline schema and the ORM-only040200carry none).Entities: new
PushSubscriptionsandGoogleApiUsage(entity count 125 to 127), the new columns on the existing entities;check:entitiesand entity-schema-parity pass.How the baseline handles an install at any legacy step:
legacy-baseline.tsderives the step map from the markers, so the only change it needed was the new files. An install with aschema_versionrow N and nomikro_orm_migrationsrows gets steps 1..N recorded as executed; the migrator then applies the baseline schema (IF NOT EXISTS),040200, and steps N+1..258 in the same transaction. N above 258 still refuses the boot.legacy-baseline.test.tsis pinned to final step 258, with a new case LEGACYBASE-007 (an install at 250 is baselined, not refused).SchemaVersionSeeder.LEGACY_SCHEMA_VERSIONmoved from 241 to 258 (see Decisions), and a newschema-version-seeder.test.ts(SEEDVER-001/002) derives the expected value from the markers so it cannot drift again.What was ported, per domain
PushSubscriptionsRepositorywithupsertSubscription,deleteOldestForUser,deleteForUserEndpoint,listForUser,countForUser,hasAnyForUser,deleteAllForUser,deleteSubscription,recordSuccess,incrementFailureCount;push-subscriptions.service.ts,WebPushServicelast inNotificationsService; dev's instance defaults innotification-preferences.service.ts(Server/Admin Settings for Notifications #1536), writes inuow.transactional.deleteAllForUserinside the existing transactions;user-profile.service.tsgetsoperatorKeyVariables()(Allow specifying Google Maps/Places API key via environment variable #1881); adminplaces-google-onlytoggle is async and audited.GoogleApiUsageRepository.callsOn,recordCall(upsertON CONFLICT(day)),purgeExpired;GoogleQuotaServicefully async overAppSettingsRepository; maps and transit awaitexhausted()/record().appSettings.getValue, quota wrapper,normalizePlaceWebsite,clampPoiBbox.BudgetItems.listMoneyRows,hasUnfrozenForeign,listUnfrozenForeignCurrencies,listUnfrozenIdsForCurrency,freezeUnfrozenForCurrency,listLinkedToReservation,listIdsByReservation,listIdAndCategoryByReservation,findIdByReservationInTrip,deleteByIds; the same unfrozen-rate methods onBudgetSettlementsplusnote;BudgetItemMembers.listForTripWithUsersAndPaid;getCurrencynow trip-scoped;getPerPersonSummary(BG71) removed because dev replaced it. Freeze-rates route and MCP toolfreeze_budget_rates.removedeletes every linked expense (deleteByIds) in one transaction and returnsdeletedBudgetItemIds;withFrozenRateresolved before and outside the write transaction; type-change re-files linked expenses.generateDaysruns dev's sharedplanDayGridoverDays.listForDayGrid(TP77) andlistDayGridStays(TP78) inuow.transactional;updateTripreturnsremovedDays; currency default viaSettingsService;searchPlacesoverTrips.searchPlaceNames(TP79);nextUpcomingTrip,setEndDateTouched,findListShapeById.appendDated,DayRemovalService.remove(cancels stays checking in or out, shifts road-trip boundaries viaRoadtripDayBoundaries.listDayNumbers/listDayNumbersFrom/moveDayNumber);resyncAccommodationDaysseats the stay's stop throughnight-seat.ts.night-seat.tsis now the pure rule (standsAhead,seatAmong,seatHolds,planViaCarry, ...) plus*Withadapters over the repositories;DayAccommodations.listIdsCheckingInOrOutOn,listRoadtripStays;DayAssignments.listSeatRows.clearDayoverlistIdsToClear/deleteForDay,setRouteExcluded, both with REST routes and MCP tools.Reservations.listRoadtripCarriers,listRoadtripUndated,ReservationEndpoints.listRoadtripTerminals, extendedDayAssignments.listRoadtripVisits.set_place_image_from_fileoverPlaces.findActiveTripFile(PL53), country/region inlistForTrip, GPX enrichment.region_code,PlaceRegions.upsertRegionWhileUnmoved,deleteRegionWhileUnmoved,listWithPlaceLocation;PlaceRegionsRepairJobon the cron registrar; public statsnext_trip.status_override,photo_location;JourneyEntries.listPhotoPlacementCandidates,nameLocationIfUnnamed;JourneyEntryPhotos.listPhotoIdsForEntry,updateEntryLinkSortOrder;JourneyPhotoCaptureService.Users.getImmichCredentials,getImmichConnectionPrefs,setImmichSettings,clearImmichSettings), EXIF capture backfill.packed_quantity; template weight, quantity and bag throughPackingTemplateItems.listForApply/insertTemplateItemandPackingItems.listExportable.VacayCompanyHolidays.updateFraction,fractionon inserts and reads,VacayEntries.halveForPlanAndDate.share_travel_only,share_hide_images;Reservations.listPublicStayPlaceIdsForShare.Trips.findAccessible; receipt queue inImportJobsServicekept inside yourwithRequestContextwrap.Plugins.listActiveNameCapabilitiesforPluginPoisService; feedpoiCategoriesandsearchProvider.backup-archive.tsandboot-restore.tsfrom dev; handles open throughopenDatabase()(db/connection.ts,temp_store = MEMORY, [BUG] Trips cannot be deleted #2518), on the better-sqlite3 allow-list;backup.impl.tsleft it.Fixes found on feat/orm itself
These are not caused by the merge; they fail on
origin/feat/ormas it stands.npm cifails at the root. 7cd9ba3 raisedclient/package.jsonto react/react-dom^19.3.0, but the rootoverridesand lock stayed at 19.2.6. Evidence:npm ci --dry-runon clean copies of the package files fails onorigin/feat/ormwithEUSAGE ... Missing: react@19.3.0 / react-dom@19.3.0 / scheduler@0.28.0 from lock fileand passes onorigin/dev; the GitHub run on the merge commit (37228001564, "Plugin Permission Facts", stepnpm ci --workspace server) failed with exactly that. Fix: overrides pinned to 19.3.0 and three lock entries updated (11 lines), so React stays single-copy at the root.npm cifails. 7cd9ba3 bumped@clack/prompts^1.8.1, semver,@types/*and vitest inplugin-sdk/package.jsonwithout the lock (lock root still says@clack/prompts ^1.7.0). The branch's own test.yml run 35533378792 failed in Plugin SDK Types (Invalid: lock file's @clack/prompts@1.7.0 does not satisfy @clack/prompts@1.8.1) and Plugin Permission Facts. Fix: lock regenerated with npm 11. With clack 1.8 the typecheck then reported 5 TS2322 insrc/cli/ui.ts;orCancelis now<T>(value: T): Exclude<T, symbol>(no casts, runtime unchanged).triangle-alert,circle-check,ellipsis).iconMarkup.parity.test.ts(identical on base, feat/orm and dev, so already red on feat/orm) now excludes lucide's new genericIconexport by name.GET /uploads/avatars/%ZZanswers 400, not 404).@mikro-orm/nestjsmounts its request-context middleware asapp.all('/{*all}'), and Express decodes that param before any handler. Fixed as your note intests/helpers/test-orm.tsrecommends:registerRequestContext: falseinapp.module.tsand a pathlessmikroOrmRequestContextmiddleware inbootstrap.tsright beforeapp.init().LEGACY_SCHEMA_VERSIONwas already one step behind. c9c40b8 added step 242 without bumping the seeder from 241, so a fresh ORM install rolled back to a 242-step release would have replayed the reseat.src+testsprogram, andeslint .died withJavaScript heap out of memoryat about 4 GB (run 37230603970). The Lint step intest.ymlnow setsNODE_OPTIONS=--max-old-space-size=8192; the rules are unchanged.no-promise-as-value.test.tsfails wheneverCI=true. typescript-eslint then infers single-run mode, builds the program from the fixture file on disk and ignores each case's code, so all 24 invalid cases report nothing (run 37231065047, Server Coverage). The test landed after the branch's last CI run, so it had never run there. Reproduced in a Linux container: 24 failed withCI=true, 50 passed without. Both testers now setdisallowAutomaticSingleRunInference: true.Decisions to confirm
Each is implemented one way; the alternative is named.
LEGACY_SCHEMA_VERSION241 to 258. It is the 1-based stamp of the last positional step (same numbering as the markers andfinalStep). Keep, or revert to 241 and accept the replay on rollback.schema_versionat N (production: 244), while fresh installs get 258. Rolling an upgraded install back to a positional-runner release replays steps N+1..258 (all guarded; checked by reading them, not by running a rollback). Your LEGACYUP-*-004 tests pin "the legacy marker stays". Should the baseline bump the row tofinalStepin the same transaction?async up()+this.execute()for static DDL in 040600 (step 245) and 041500 (step 254), where you mostly useoverride up(): void+addSql. You have execute-only precedents (033900, 025000, 035600). Convert or keep.reseat-booked-nights.tschanged. It carries dev's body (81292d4: one pass, nights ordered bycheck_in IS NOT NULL, check_in, id), and it now owns the six prepared statements that dev had put innest/accommodations/night-seat.ts, sosrc/nestspells no SQL. It imports the pure rule fromnight-seat.ts. This changes what step 242 does for installs that have not run it yet. Accept, or keep your original body.createTestMikroOrmModule) keep the module default. Should one shared option object move intomikro-orm.config.tsso both use identical wiring?BudgetController.updateruns dev'slinkRefusal(400) before your U6toRowIdgate (404), so a non-numeric:idwith a foreignreservation_idanswers 400 as on dev. One unit pin added. If "parse:idfirst" is a hard rule, this one handler and its pin revert.ReservationsControllerkeeps dev's 404-then-400 order on purpose.withRequestContext.AtlasService.cacheRegionsInBackground(your atlas IIFE was deliberately bare) and the threeJourneyPhotoCaptureService.schedule*methods now fork their own context, withMikroORMas a required last parameter. Tests ATLAS-CTX-001 and JPCAP-CTX-001..003 fail with the wraps removed.atlas-geocode-request-context.test.tshad its spy onupsertRegion, which nothing calls any more; it moved toupsertRegionWhileUnmoved. Revert the atlas wrap if you prefer it bare (one line plus ATLAS-CTX-001).DayAssignments.reanchorToDay(withDayAssignmentsKyselyDB),DayAssignments.listLocatedIds,Days.clearDate/isEmptyDay/listTrailingEmptyIds(with DAYREPO-021..024),PlaceRegions.upsertRegion.BudgetItems.findIdAndCategoryByReservationbecamelistIdAndCategoryByReservation(dev's.all,ORDER BY idadded for a stable order). Restore any of them fromorigin/feat/ormif you want them as general helpers. Four methods already uncalled on feat/orm were left alone:OauthClients.findPublicById,PluginEntityMetadata.countForEntity,PluginScheduledTasks.existsForPluginAndName,PluginScheduledTasks.upsertTask.PhotoCaptureBackfillService.schedulehas no caller on either branch.Users.setImmichSettingsis one UPDATE again, via a new dialect helpercoalesceOverrideWhileSameinsql-functions.tsthat renders dev'sCASE WHEN immich_url IS ? THEN COALESCE(?, immich_allow_insecure_tls) ELSE COALESCE(?, 0) END. USERSREPO-086 compares it with dev's literal statement over 24 combinations. SQLite only: Postgres will needIS NOT DISTINCT FROM.client/andplugin-sdk/to dev's ranges.src/db/repositories/**at 99/99/99/99 fails on feat/orm already (see Verification). Add repository branch tests, or re-pin from a green Linux run withscripts/coverage-thresholds.mjs. Nothing was lowered.sharedprettier drift.format:checkfails on feat/orm (363 files) and on dev (430). One format-only commit on dev would fix both branches; doing it here would make 430 dev files diverge from dev.tsxis a devDependency on this branch (dev has it independencies). The Dockerfile runtime stage installs with--omit=dev, and its comment says encryption-key rotation runsnode --import tsx scripts/migrate-encryption.ts; that would fail with module-not-found in the image. Move it back, or change the rotation path.scheduleForEntry: dev called the syncjourneyIdOfEntrybefore detaching, so a throw reached the REST caller; herefillForEntryawaits it inside the detached chain and logs[Journey] capture refresh failed for entry <id>:(pinned by JPCAP-003b). The entry add has already succeeded at that point.budget.service.ts(tags only in repository docstrings);getImmichAutoUploadtagged JV1 (its only caller is JourneyService);BudgetItems.deleteByIdswritten in Kysely like its neighbourdeleteById;insertTemplateItemnames each column in.values();DayGridReadRow.has_plan_itemstypednumberwith$castTo;calendar.service.tslegsOf = (r: any)kept as dev wrote it next to yourwindowSidesOf = (r: any)(type both or neither);GoogleApiUsage.daytypedstring | nullbecause the generator check requires it; a docstring-only edit in the shipped migration 040300;backup-archive.tsruns two rawprepare()calls (PRAGMA integrity_check,sqlite_master) on the uploaded archive's own handle, not the app DB.tests/fixtures/legacy/legacy-v258.sqlwith an integration test would pin a dev-install upgrade permanently; today LEGACYBASE-007 and the place-regions upgrade cases cover it in unit tests, plus the one-off boot matrix below.Verification
Local, Windows, on the tree of the first two commits (the last round ran on the index with no unstaged or untracked files):
typecheck,typecheck:tests,typecheck:scripts: 0 errors.no-explicit-anyin 23 test files,legsOf(r: any), 11require()insrc/index.ts, one unused var); client 0 errors, 1209 warnings;lint:pagesOK.check:entities,db:call-graph -- --sync --tx,check:plugin-factsandi18n:parity:strictpass.vitest run --coverage --maxWorkers=4, private TEMP): 732 files, 15747 passed, 8 failed, 21 skipped. The 8 are the known Windows baseline: 6 inbackup.impl.test.ts(path separators), 1 inauto-backup.test.ts, 1 symlink EPERM instorage/local.driver.test.ts. e2e (tests/e2e, 53 files) is inside that run and passed fully.node_modules, not 19.3.0.npm ci,typecheck,buildclean; 340 passed, 13 skipped (monorepo and registry parity suites).src/db/repositories/**: branches 89.75 < 99. This one is not Windows-specific: repository files the merge leaves byte-identical already sit at 562/618 branches (90.94%), so the entry fails on feat/orm too. af533fc adds test M1b for the new Immich reads.src/nest/backup/**: statements 96.62 < 97, branches 92.14 < 93, lines 97.78 < 98. The misses concentrate inbackup.impl.tswhere the 6 Windows-failing tests abort, so it is expected to pass on Linux; not confirmed yet. af533fc adds BOOT-RESTORE-008 for dev's newboot-restore.ts.buildApp()andapp.init()on the built server: 38 boots, all OK. For every N the log reports baselining N migrations and applying 260 minus N; afterwardsmikro_orm_migrationshas 260 rows, the first N are steps 1..N in step order,schema_versionstays N, and nothing refuses. The fresh file applies all 260 and is stamped 258. Boot times 6.0 to 7.4 s.trek_photo_cache_meta.cache_keyNOT NULL, which is this branch's own040200(the same single difference appears between a fresh base bf0507d and a freshorigin/feat/orm). Both triggers exist exactly once on every upgraded database.cache_keyNOT NULL.test.ymltriggers on pull requests tomainanddevonly, so this PR (basefeat/orm) shows just the target-branch check; the suite runs byworkflow_dispatchonorm-sync-dev.npm ciand plugin-sdknpm cifailed (fixes 1 and 2).eslint .withJavaScript heap out of memory(fix 6), so the coverage jobs were skipped.no-promise-as-value.test.ts(fix 7). The Windows-only failures above pass on Linux.src/nest/backup/**meets its pin on Linux. The one miss issrc/db/repositories/**branches 89.83% against 99% (Decision 12), which feat/orm never measured in CI; the pin was not lowered.Known pre-existing issues left alone
users.mfa_backup_codesmissing (MFA backup codes may fail on production),users.immich_access_tokenmissing,reservations.accommodation_idINTEGER with an FK today_accommodationsinstead of TEXT without FK,budget_items.categorydefault'Sonstiges'instead of'Other'.sharedformat:checkfails with 6667 errors; every file with new drift relative to feat/orm is byte-identical toorigin/dev, andshared/equals dev apart frompackage.json.src/db/repositories/**coverage below its 99% branch pin on feat/orm (above).settings.e2e(week_start test) logsAudit write failed: no such table: audit_log; the fixture has noaudit_logtable, the test passes.Reservations.repository.tslistUnplannedPlacesForPublicApi: unusedeblint warning, same line on feat/orm.allowScriptsgating skipped install scripts duringnpm ci(better-sqlite3 13 loads from its N-API prebuild; nothing else failed);npm auditreports 15 vulnerabilities in the merged tree (1 low, 6 moderate, 7 high, 1 critical), not addressed here.tests/integration/legacy-upgrade-v242.test.tsheader says only post-legacy migrations run at 242 (steps 243-258 now run there too; suite passes 5/5);vitest.config.ts:30still says "replaying 242 migrations".