Commit Graph

301 Commits

Author SHA1 Message Date
elmocito 8739b4c74d fix(tours): restrict permanent deletion to tours workspace 2026-10-06 12:52:25 +02:00
elmocito af7dbce105 fix(tours): prevent duplicate day assignments 2026-10-05 23:53:11 +02:00
Maurice c92478c5ad feat(tours): align the Tours UI with the planner redesign
The Tours surfaces now use the same building blocks as the rest of the
planner: tinted head bands, eyebrow labels, white field boxes and pills,
place-style list rows and the inspector frame for the map detail.

- Tour detail on the map uses the place inspector's frame, head band,
  labelled metric fields, collapsible track colour and footer buttons
- Tour rows in the Plan sidebar and the Tours tab look like place rows,
  with the facts as pills and a "+" that opens the trip's days as a menu
- Tours tab rails get the planner head bands, editor fields and
  waypoint rows in the day card style; notices share one look
- Map hint shortened to one line and the empty state reads as a single
  sentence in all locales
- New "tours" mascot scene with a pack, a trekking pole, hills and a
  trail, used by the Tours empty states
2026-10-05 12:04:49 +02:00
elmocito 2ec5ae7e4d merge: update Tours branch with current Places filters 2026-10-05 01:50:47 +02:00
EvKoh b6115d48f3 refactor(places): one name for the uncategorized bucket and the category 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.
2026-10-04 16:20:17 +02:00
EvKoh 4b263c8e4b fix(mobile): align the places browser's filter panel with the map control
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.
2026-10-04 16:20:17 +02:00
EvKoh eee361ef28 docs(places): say where the map's places filter departs from the lists
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.
2026-10-04 16:20:17 +02:00
EvKoh db3c9d660e fix(mobile): announce which Show and rating chip is chosen
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.
2026-10-04 16:20:17 +02:00
EvKoh 3951e8cc9a fix(map): announce the places filter control as opening a dialog
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.
2026-10-04 16:20:17 +02:00
EvKoh a9f7adf99b fix(map): move the tracks filter fallback to the always-mounted planner 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.
2026-10-04 16:20:16 +02:00
EvKoh fe5a8a8854 fix(map): make the places filter sheet opaque
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.
2026-10-04 16:20:16 +02:00
EvKoh 66f11602b2 feat(map): filter trip places from the mobile map
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.
2026-10-04 16:20:16 +02:00
EvKoh a44c966f4d feat(places): share the rating floor between the lists and the map
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.
2026-10-04 16:20:16 +02:00
Maurice 5bcb377b9c fix(mobile): trim the note wrap classes to one rule and cover the kept 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.
2026-10-04 15:46:10 +02:00
Diogo Mendes a9c78f680e fix(mobile): prevent layout breaking from long unbroken text in notes 2026-10-04 15:46:09 +02:00
elmocito 9e475f447a feat(tours): add non-motorized day tour planning 2026-10-04 12:59:55 +02:00
Maurice b6b4904664 feat(bookings): cruises list their ports of call with arrival and departure (#1807) 2026-10-01 14:31:12 +02:00
Maurice db4337287b fix: mark deliberately unawaited promises with void (Sonar S9383) 2026-10-01 14:19:52 +02:00
Maurice 99c62878f3 feat(pwa): a relaunch also returns to the plan day it was on (#666) 2026-10-01 13:22:37 +02:00
Maurice 855c0a4b24 feat(settings): optional preferred map app for the navigate button (#2423) 2026-10-01 12:52:58 +02:00
Maurice f4626c9f3e feat(notifications): admin defaults for users, and channels blocked for everyone
Admin > Notifications gets a Defaults for users matrix: each event and channel starts On, Off (a user may still turn it on) or Blocked (off for everyone). A user's own choice is kept against the default, and a value equal to it is stored as no row, so a later change of the default reaches everyone who never chose. Blocked cells show as locked in the user settings on desktop and phone, and the server ignores any attempt to turn one on. (#1536)
2026-10-01 12:28:36 +02:00
Maurice 885896aba2 feat(files): FILE_UPLOAD_LIMIT_MB sets the upload limit for files and notes
The 50 MB limit for files, booking attachments and note attachments becomes FILE_UPLOAD_LIMIT_MB, 50 by default. The server hands it to the client through the app config, so the upload dialogs refuse a file over it up front and say what the limit is; on desktop a file over it no longer vanishes without a word. (#1364)
2026-10-01 11:56:14 +02:00
Maurice 52934a8326 fix(mobile): the map compass stands above the base-layer switcher
Beside it the two round controls read as one crowded row over the dock; on the switcher's own left edge, one gap above it, they form a column.
2026-10-01 10:38:29 +02:00
Maurice ba0cc331c1 feat(mobile): today marked on the day rail, and a button back to it
While a trip is running, today's chip on the phone carries a ring and a dot, and a button beside the rail jumps back to today from wherever it was scrolled, in the plan and on the map. It stays in place, dimmed, while today is open, so the rail never shifts under the thumb. (#2392)
2026-10-01 10:17:29 +02:00
Maurice 48f28b40c4 feat(collections): navigate to a saved place from its sheet on the phone
The phone's collection place sheet gets the Navigation button the desktop detail and the trip's place sheet already have, first among its actions: one offered app opens straight away, more open the same picker. (#2091)
2026-10-01 10:17:29 +02:00
Maurice 542a57b74e feat(auth): password rules checked while typing, with the server's own policy
The password policy moves to @trek/shared, where the server keeps enforcing it unchanged and every form that sets a password now runs it before sending: the forced change after the first login, registration, the reset link, the account settings on desktop and phone, and creating or editing a user in admin. Under each new password a checklist ticks off length, upper and lower case, a number and a special character, and a refusal names the actual reason, a too common or too repetitive password included. (#1200)
2026-10-01 10:17:28 +02:00
Maurice c80ee6b12a feat(planner): reach a hotel's booking from the plan, not only its place
A booked stay's pill in the day head gets a second target that opens the booking; the name still opens the place. The place panel of a hotel, on the desktop and on the phone, lists the stay booked for it next to the bookings pinned to the stop. (#2363)
2026-10-01 10:17:28 +02:00
Maurice f7782b9ef7 feat(planner): the places search also finds the description and the notes
The search box of the places list matched only name and address. It now also reads what was written about a place, through one helper the desktop list, its filter counts and the phone's browser share. (#2252)
2026-10-01 09:30:42 +02:00
Maurice d6435572b7 fix(journey): two quick swipes in the photo viewer turn pages instead of zooming
A double tap counted every lift of a finger, the end of a swipe included, so
two quick swipes zoomed in. Only a touch that stays in place is a tap now.
The client tests follow the fields and arguments the qol sweep added.
2026-09-30 23:30:51 +02:00
Maurice 8fd46c4c18 feat: quality of life sweep from the feature discussions
Small requests from the discussions, each built end to end: planner and
map (cycling mode, date first headings, clear a day, stay times per day,
off-route stops, map lock, compact unplanned markers, pending stays,
cable car, transit quick picks), places (location filter, category from
OSM, open in maps, geo: link, image from an attachment, enrichment on
file import, places near a pin, place name language, Amap first in
China), lists and costs (packed counter, inherited sharing, Markdown
to-dos, payment notes, bookings CSV, my plan PDF), sharing (unplanned
places, travel only, without photos), journey (photo zoom, drafts,
photo order, trip suggestion, day jump), atlas (region counter, region
bucket list, places per country), vacay half company holidays, studio
copy and paste plus the book's own layouts, and RESTORE_FROM_BACKUP for
a first start restore.
2026-09-30 23:00:12 +02:00
Maurice 91dfab3f4b fix(planner): hand the day plan's unawaited calls to void
Sonar counts a promise that is neither awaited nor caught as a reliability bug, and it reads every such call in a touched file as new. Each of these already reports its own failure (toast, rollback or the sheet's catch), so they are marked as fire and forget rather than changed.
2026-09-29 21:55:03 +02:00
Luffy 7d22857de8 fix(mobile): restore day selection when reopening a trip 2026-09-29 21:48:26 +02:00
Maurice 5f700615b9 fix(mobile): files attached while editing a booking are uploaded on save
The phone booking and transport sheets collect picked files and upload them
after the save, but only when the record was new. Editing an existing booking
showed the picked files in the sheet, then dropped them on Update without a
word. The upload now runs after every save that comes back with a record,
through one helper both sheets share. A save that fails uploads nothing.

Closes #2534
2026-09-29 21:16:26 +02:00
Maurice 237f42a493 fix(llm-parse): harden photo reading and finish the receipt scan
A photo is now decoded only when its header says it is within 40 megapixels,
and bytes TREK cannot shrink, such as a WebP, are sent only up to 3.5 MB. A
scanned PDF page is drawn by its long edge, and pdf.js skips an embedded image
above the same pixel cap instead of decoding it.

The receipt reading is held to bounds in the shared contract, keeps the minus
sign of a discount and leaves such lines out so the Ticket split can save, and
its prompt states the JSON shape, so a server without json_schema still answers
something the parser reads. The model lookup sends the configured key, the
import error names the photo formats, the scanned photo is attached only for
someone who may upload files, and the phone settlement reloads after a receipt
is saved from the trip sheets. Test ids that clashed after the rebase are
renumbered, and the wiki describes the image setting, photo import and receipt
scan.
2026-09-26 15:59:05 +02:00
RemEx b1c05bca83 feat(llm-parse): read a photo, as a booking or as a receipt in Costs
Whether the AI Parsing model reads images is an admin setting now, and it
lands with the two features that send a photo, so it has something to
control.

The setting is `vision: 'auto' | 'on' | 'off'` on the addon config. On
Automatic a local Ollama is asked what the model can do (`/api/show`), and a
cloud provider means no; Yes and No are applied as set. The old `multimodal`
flag is not read: the admin screens wrote it false on every save, so a
stored false meant nothing. The per-user toggle keeps its on/off meaning and
is read by the extraction now. The lookup is server-side only: capped read,
validated body, never throws, cached per server and model.
GET /api/llm/capabilities answers it for the signed-in caller, and both
photo pickers below ask it.

The booking import takes JPG, PNG and WEBP when the model reads images.
The photo goes as an image to every provider (image_url, an Anthropic image
block, Ollama's native chat with `images`), capped at 1600px, and skips KDE
Itinerary. A scanned PDF, which used to reach the model as empty text, has
its first two pages drawn by pdf-parse and sent the same way. The rest is
the import as it was: background job, review, the file attached to the
booking, the linked cost. On a real qwen3.5:4b a train ticket came back as
bare clock times until the router gave the model every type's field
meanings, and its price as an object inside a string, which read as 4900;
both are handled.

Costs gets Scan receipt: a dialog laid out like the booking import's takes
one photo, read on the import job queue, then reviewed in the existing
expense editor pre-filled with merchant, total, currency, day and lines,
the photo waiting in Receipts & Invoices to be uploaded on save. No table,
no migration, no category guess, no document-type detection.
2026-09-26 14:38:01 +02:00
Maurice d1dc8542ac fix(costs): leave out an expense no rate is known for instead of counting it 1:1
A foreign expense without a frozen rate is converted with today's rate.
When the server could not fetch rates itself, it counted the amount 1:1
in the trip currency, although the browser had the rates all along. A
VND bill on an AUD trip was settled as millions of AUD.

Such a row is now left out of the balances, flows, final budgets, trip
totals and per-person summaries. The settlement reports it under
`unconverted` and says which currency its figures are in; the trip
summary lists the ids and the budget prompt says how many are missing.

The browser lends the rates it already holds (`fallback_fx`) when it
saves an expense or a settle-up transfer, and to heal existing rows
through the new POST .../budget/freeze-rates route, which an editor's
Costs view calls for the rows the settlement left out. The server still
prefers its own rate, uses a lent one only while it is quoted against
the trip currency, and never replaces a frozen or explicit rate. The
settlement also takes the browser's display rate (`base_rate`) in place
of a display quote the server cannot fetch; the browser leaves out a
figure the query would refuse. The browser's own fetch gives up after
10 s.

Changing a row's currency while no rate is available no longer keeps
the old currency's rate; the row goes back to unfrozen.

MCP gets the same through the new freeze_budget_rates tool, which only
ever pins the server's own rate.
2026-09-26 13:19:27 +02:00
Maurice 44a12e21fc fix(costs): read an expense entered in the display currency as it was typed (#2525)
A euro trip read in dollars books a bill entered in dollars at the rate
of the day, from the euro's quote: 12,345.67 USD at 1.1398 is 10,831.44
EUR. The Costs screen and the settlement then turned those euros back
into dollars with the dollar's own quote, 1 / 0.87732. Frankfurter
rounds the two quotes separately, so they are not exact inverses, and
the bill read $12,346.05 the moment it was saved, a $250.00 one $250.01.
The error grows with the amount, which is why the large rows showed it
first. Paying back what settle-up offered for such a bill left the trip
open with the debt flipped the other way.

Both sides now convert trip money to the display currency with the trip
currency's own quote, the one every entry rate is frozen from. On the
server, settlement() fetches that quote for the REST route and for the
MCP get_settlement_summary tool, which now makes the same call, and a
legacy transfer without a currency reads back through the same factor.
On the client, useExchangeRates takes the trip currency as an anchor
and converts through its cached quote, falling back to the display
currency's while it is missing, so it works offline from cache and
needs no change to the settlement response. The final budget builds
each row from the unrounded trip amount, because a euro cent is worth
more than a dollar cent and 10,831.44 EUR came back as 12,345.68; the
balances and settle-up still net whole trip cents.

A same-day bill in the display currency now reads exactly as typed on
the row, the payer chip, the day header, the totals, Outstanding and
the final budget, on both shells and on the shared trip page, and
paying the amount settle-up offers leaves both balances at zero. When
the rate really moved, the row keeps the value the totals count, with
what was entered and the euros it was booked at underneath, "$801.76 ->
685,26 EUR". One helper builds that line for the desktop list, the
phone list, payments, the final budget breakdown and the shared page;
it prints nothing for an amount of 0 or a bill that reads as typed.

The expense editors share one module for their preview. The split
summary puts the share the row lends or borrows next to the entered
one, "$400.88 -> $390.53 each", and an expense saved without a currency
still opens in the trip currency. Both CSV exports add the amount in
the trip currency when it differs from the display currency.

Kept from before: the desktop totals recompute when rates arrive, a
booking's linked costs are labelled in the trip currency (every linked
row, the link picker and the phone's linked costs), a legacy
transfer is read in the display currency, the shared page reads the
frozen rate in whole cents, a booking import keeps the currency it was
quoted in at a rate frozen before the write (REST and plugin RPC), and
the MCP trip summary, the budget-overview prompt and the per-person
summary add every row up in trip cents at its frozen rate. The
packing-list prompt reads the trip title directly instead of building
the whole summary, which could wait on a rates fetch.
2026-09-26 10:36:03 +02:00
Maurice 8b6a8d3098 fix(mobile): show the drive from a flight's arrival airport in the plan timeline (#2502)
The phone timeline only put a travel-time connector between two places.
The drive from the arrival airport to the first stop after a flight is
part of the day's route and drawn on the map, and the desktop day plan
shows it under the flight, but on the phone the two rows sat on top of
each other. The drive from the last stop to the departure airport was
dropped the same way, and so was every drive past a booking saved
without a location, like a taxi to the airport or a concert between two
stops.

buildPlanRows now walks the rows the way the route calculation walks
the day: a located booking ends the drive before it at its departure
point and starts the next one at its arrival point, and each leg goes
under the row it leaves from. The route rides straight past a booking
it has no location for, so the drive through it goes under that
booking, as on the desktop, and keeps the mode menu of the stop it
left. Only transport bookings count as located, as in the route
calculation. Two bookings back to back still get no road between them.
A booking whose station is the stop itself, as the transit planner
saves a journey between two of the day's places, leaves a leg of no
length, and that one gets no connector.
2026-09-26 10:36:03 +02:00
Maurice 62f8b0deee fix(mobile): pick the plan timeline's hotel legs by what the route drew (#2501)
On a day that opens with a flight, where the traveller also put the
hotel on the day as a stop of their own, the phone timeline started the
day with a hotel row and the drive from that stop to the next place,
above the flight, and then showed the same drive again in its own spot.

hotelLegsForDay guessed the bookends from coordinates: the first
segment leaving the hotel's spot was the morning leg. The route had
correctly drawn no morning leg on that day, but the stop on the
hotel's spot starts a leg there too, so that hop was taken for it.

The route calculation now tags the two legs it draws from and to the
accommodation, and the timeline reads that tag instead. The hotel end
of a tagged leg still has to be the day's own stay, because the
calculation keeps the previous day's legs until the new day has
routed. The stop to stop connectors leave tagged legs alone, so one
leg can no longer show up in both places.
2026-09-26 10:36:03 +02:00
Maurice 287b84875b fix(budget): show the link controls on an explicit null check
targetId is a number or null; a bare && would render a 0 if it were ever 0 (Sonar S6439).
2026-09-25 16:25:30 +02:00
Maurice 8864edda18 feat(budget): link existing expenses to bookings and places, several per booking
Discussion #2084: a cost bought later (the seat upgrade for a flight) could
not be tied to the booking it belongs to, and a booking held one expense.

- The Costs block of the booking, transport and place forms lists every
  linked expense with edit, unlink (the expense stays in Costs) and delete,
  and offers the expenses that belong nowhere yet under Link existing
  expense. The phone sheets get the same block in their own design, and
  their Files row lists the attached files and links an existing one.
- PUT /budget/:id, update_budget_item (MCP) and costs.update (plugin RPC)
  take reservation_id / place_id; create and update refuse an id from
  another trip on all three paths.
- The booking's mirrored price is the sum of its expenses when they share
  one currency (none counts as the trip's), else the first one, with that
  currency, and follows every change to a linked expense. The booking
  form's price field leaves a booking with several expenses alone.
- Deleting a booking deletes all of its linked expenses and broadcasts each
  one, also from the MCP delete tools.
- Cards show the price with its currency.
- Booking and transport dialogs: Booking Type and Travelers as dropdowns at
  the top, the URL beside Files (transports gain the URL field). The row is
  one shared component; linking and unlinking files go through the store.

Help center pictures and texts for the booking, transport and expense
guides are redone in all 27 languages, five Costs labels that were still
English in 16 languages are translated, and the wiki follows.
2026-09-25 15:34:32 +02:00
Maurice 5c312a8497 feat(packing): print, PDF and Markdown/CSV export of packing lists
The packing list bar gets an export menu next to the import button, both
now icon only. The menu prints the open view as a TREK styled page (trip,
dates, packed count, one card per category, bags), which the browser can
also save as a PDF, and downloads it as a Markdown checklist or as a CSV
in the exact format the import reads, bags included, so a list works as a
personal template without admin rights.

The import now also reads Markdown checklists: headings become
categories, "- [x]" marks an item packed, a "3x" prefix sets the
quantity and a "(200 g)" suffix the weight. The phone action sheet
offers the same exports through the shared usePackingExport hook.

The help center gets an export guide with new pictures, the import guide
and the wiki page describe the Markdown format, in all 27 languages.

Addresses discussions #1420 and #875.
2026-09-25 11:31:23 +02:00
Maurice 2a8071f642 feat(plugins): search plugins can answer the place search as you type
A searchProvider plugin can now implement an optional suggest() beside
search(). Its places then show up in the place search's typed-ahead list,
after the core suggestions and marked with the plugin's name, on desktop
and phone. An index that cannot take a request per keystroke leaves it
out and keeps answering the full search only.

The child reports which functions each hook object carries at load, so
GET /api/plugin-search/suggest only asks the plugins that implement
suggest: from the second character, three rows at most across all of
them, 800 ms to answer. The feed marks plugins holding
hook:search-provider and the client only asks the route while one is
installed. A plugin row carries its whole place, so picking it skips the
details lookup, which knows no plugin id. The phone's search results now
name their source as the desktop ones do, and the desktop dropdown
scrolls instead of running past the dialog.

Addresses discussion #2221.
2026-09-25 09:19:11 +02:00
Maurice 1727732841 feat(plugins): plugins can add POI categories to the map and answer them
A new manifest capability poiCategories (up to 4, icon from an allow-list,
strict #rrggbb color) and the hook poiCategoryProvider.getPois behind the
hook:poi-category-provider permission. GET /api/plugin-pois in the plugin
contributions module answers them in the /api/maps/pois shape, with
search_plugin_pois as the MCP twin. plugin: ids no longer count as Google
place ids. SDK 1.8.0 carries the types, validation and docs.

On the map, one hook merges the built-in categories with the ones active
plugins declare, and the pill shows the plugin chips after a divider on
desktop and phone, scrolling sideways instead of shrinking a chip below
its touch target. A plugin chip searches through a repo that refuses
offline and parses the answer strictly. Its pins, on Leaflet and GL, use
the category's own icon and color, both checked again before they reach
any markup, and the hover card lists the plugin's details as escaped
text. A plugin POI can be added as a place like any other. The admin
panels on desktop and phone show a chip for the new permission and the
categories a plugin would add, from one shared module.

Addresses discussion #1781.
2026-09-25 01:24:55 +00:00
Maurice c7c9ba24a3 fix(journey): select all takes every photo in the range and imports keep their order
The photo picker's Select all took only the pages already loaded, 50 at a
time, and replaced whatever had been picked on another tab. It now loads
the rest of a day, trip or date-range search before selecting (cancelled
by closing the picker or switching tab), adds to earlier picks, and
Deselect all only clears what is in view. Add stays disabled until that
loading is done. The whole-library tab keeps selecting what is loaded and
shows the count as "N+".

A date-filtered Immich search stopped at 1,000 photos without a word,
because its page budget was absolute; it is now relative to the page
asked for, with an absolute ceiling of 270 raw pages sized so the
picker's deepest page still gets a full answer. Adds go out in batches of
500 ids, under the JSON body limit, and each batch is sent oldest first,
so photos land in an entry in the order they were taken. When a later
batch fails, the photos already saved are counted and the journey
reloads while the error still shows, through one hook for desktop and
phone.

After a provider add, the capture-time lookup runs at most four at a time
per user, shared by all of that user's batches. When it fills a row, a
journey:photos:updated event reloads open journeys once per batch, from
the REST routes and the MCP add tool alike. Device uploads, which arrive
one file per request, get the same lookup without the event.

Addresses discussion #1587.
2026-09-24 23:48:49 +00:00
Maurice 650451bdf7 feat(notifications): Web Push delivers notifications to every device, even with TREK closed
A fourth built-in channel next to email, webhook and ntfy. Browsers
register per device, the server encrypts per RFC 8291 and signs per RFC
8292 with node:crypto and sends only to known push services through the
SSRF guard. The event matrix decides per category what arrives by push;
admins switch the channel on under Notifications.

VAPID keys come from the environment or are generated once, only while
neither stored row exists, and kept encrypted. A pair that does not
decrypt or match is never overwritten and never swapped for another one:
push stays off with an error in the log and every device stays
registered until the key is fixed. The subject is VAPID_SUBJECT, then an
https APP_URL, then the project URL.

Only chat messages collapse into one notification per trip, and a
replacement still alerts. A 403 removes a device only after three
failures in a row; 404 and 410 remove it at once. Password change, reset
and an admin setting a password drop the user's devices with the other
credentials. The request log redacts subscriptions and endpoints, boot
errors print *** for every secret variable, and logout forgets the
device with a plain fetch so an expired session cannot redirect midway.

Addresses discussion #894.
2026-09-24 23:17:03 +00:00
Maurice 7ce31756a3 feat(settings): every date picker starts its week on the day you pick
Settings > General > Language & region gets "Week starts on" (Monday,
Sunday or Saturday), and Admin > User Defaults a default for everyone who
has not picked one. Monday stays the built-in value, so nothing changes
for anyone who leaves it alone.

Both calendar implementations follow it: CustomDatePicker, used by trip
dates, bookings, costs, to-dos and the admin school holidays on desktop
and phone, and the journey date picker, whose weekday headers were
hardcoded English and now speak the app language. The grid math and the
header row live in one helper (utils/calendarWeek.ts). The day names on
the options come from Intl, so only the label and a hint needed
translating. Vacay keeps its own per-plan week start.

The value is validated like the other regional settings: VALID_VALUES for
admin defaults, the display-settings MCP tool, and the managed key list.

Addresses discussion #2029.
2026-09-24 21:56:12 +02:00
pipipiper 196dc12762 feat(mobile): place details block in the place editor 2026-09-24 21:40:13 +02:00
Maurice 8906b66bc8 feat(settings): set the Google Maps key through PLACES_API_KEY
The server already put PLACES_API_KEY ahead of the key saved in the admin
panel, but nothing said so: the variable was in no template and the admin
field kept showing a stored value that no search used.

GET /me/settings now answers env_keys, the variable behind each key the
environment sets (Google, Unsplash, Amap), and null for that key's value.
The admin field turns read-only and names the variable, Test still probes
the key a search resolves to, and a save leaves those names out so the
stored fallback survives. The variable is documented in .env.example,
docker-compose.yml, the Helm chart (as a secret) and the wiki.

Addresses discussion #1881.
2026-09-24 16:50:02 +02:00
Maurice e1315806c2 feat(roadtrip): the ride block and the stay rows read as badges
The flight block in the road trip rail printed each fact two or three times: the booking
title in capitals with its duration cut off, the airport code three times per side, an
unlabelled 14:15 on the right with "Departure 15:15" under it, 17:20 twice, and a warning
"+9 h 39 min" measured against a check-in nobody could see, for a flight that was missed.

It now reads in four lines: the booking as typed; the two codes with their timetable clocks
and the ride between them; the places without the code in brackets; and the check-in as a
badge that carries its own state, the hour when the drive is on time, by how much when it
is late for the check-in, and "Missed, there at 23:54" when it gets there after the
departure, with the duration beside it. A missed ride edges the block and says so in the
day header, also when the day is folded. Trains and buses say boarding. The phone shows
the same, and its rows now read the lateness and missed-leave findings at all.

The stays at the edge of a day are named by the hotel, with the edge in a badge: Check-out
with the latest hour, Day start, Overnight, or Check-in with the hour the room is ready. A
plan that leaves after the check-out turns that badge instead of adding a line. They wear
the hotel stop's own disc, the stay's own stop shows its check-in the same way, and the
driving settings button shows when the stays are switched on.

The two-part badge the rail used in six hand-typed copies is one FigureBadge now, and the
airport code is stripped by one shared helper instead of two private copies.
2026-09-24 11:32:52 +02:00