- Uninstall with data disposition: POST /api/admin/plugins/:id/uninstall kills the
plugin, removes its code + DB metadata, and (deleteData) drops its data dir,
error log and per-user settings.
- Error log: GET/DELETE /api/admin/plugins/:id/errors — the plugin's own crash /
request-failure log, surfaced in the admin panel.
- Egress guard: the isolated child wraps global fetch and refuses any outbound
host not in the plugin's declared egress[]; with none declared, all outbound is
blocked. Process-level defense in depth (the container runtime enforces it at
the network layer in v2).
- Admin → Plugins is now actionable: activate / deactivate / uninstall, a
registry browser (install), and per-plugin error log. i18n across all locales.
- Dashboard widget slot: active widget plugins render as sandboxed cards.
The trek:request core-data bridge + photo/calendar hook consumers remain follow-ups.
Plugins can now render UI. Page plugins appear as a nav entry and open a
full-page sandboxed iframe; the frame talks to TREK only over the postMessage
bridge.
- Server serves plugin client assets at /plugin-frame/:id/* with a strict path
guard and a locked-down, per-plugin CSP (default-src none; connect-src limited
to declared outbound hosts; sandbox WITHOUT allow-same-origin -> opaque origin,
so the frame can't read the session cookie or the parent DOM). Global CSP
frameSrc relaxed from 'none' to 'self' for exactly these frames.
- GET /api/plugins feed lists active plugins for the client.
- Client: pluginStore + PluginFrame (the trekBridge host) authenticates every
inbound message by SENDER WINDOW IDENTITY (event.source), not by a claimed id
or origin; pushes context (theme/locale/tripId/userId), validates navigation,
renders notifications as text, resizes widgets, and proxies trek:invoke to the
plugin's own routes host-side (session cookie stays with the host).
- Page route /plugins/:id + Navbar nav injection for page plugins.
Dashboard widget slot and the trek:request core-data bridge are deferred to a
later slice.
First slice of the plugin system. Lays down the data model and a read-only
admin surface; nothing executes yet.
- Migration 155: plugins, plugin_meta_migrations, plugin_error_log and
plugin_settings_fields tables. Plugin data will live in a per-plugin sqlite
file under /plugins-data, never in these tables.
- New Nest module server/src/nest/plugins with GET /api/admin/plugins
(admin-gated, returns the installed list + the runtime-enabled flag).
- TREK_PLUGINS_ENABLED kill switch (config.pluginsEnabled), off by default.
- Admin → Plugins tab with a read-only panel: installed list, status badges,
and a clear banner when the runtime is disabled by server config.
- i18n keys for the tab and panel across all locales.
Install, activation, the isolated runtime and the registry browser follow in
later slices.
Transit entries were classified as bookings by the planner's transport-type
list and landed in the Bookings tab — they now sit in the Transports tab's
own section, rendered as proper journey cards (tram icon, arrow title, leg
chips, journey stats) instead of the generic booking card.
The journey modal got a redesign: the title renames inline in the header
with an icon arrow, the stats become three full-width tiles (duration /
transfers / walking, each with an icon), status and booking-code fields are
gone, and notes take the full width with a markdown write/preview toggle.
The Automated search mode gains a proper header (icon, hint, day picker)
and the day-plan row now shows walks with their minutes inside the chip
sequence (🚶 3 › U2 › 🚶 3) instead of a detached direct/walk summary.
"A → B" titles render with an arrow icon everywhere. The Transports tab's
add button is simply "Transport" now that the dialog covers both modes.
The add-transport dialog gains a Manual/Automated switch: Automated embeds the
public-transit search (day picker + from/to + modes + preferences + results)
right in the dialog, and the day header's tram button opens it directly in
that mode. The standalone search modal is gone.
Saved journeys get their own roomy journey view — the stop-by-stop itinerary
(times, platforms, lines, headsigns, operators) together with the editable
booking fields, delete, and a "Change route" action that re-runs the search
pre-seeded with the journey's origin/destination and replaces the itinerary on
save. The generic transport form no longer opens for transit entries, from the
timeline or from the Transports tab, where journeys now sit in their own
"Automated public transit" section with their line badges on the card.
New i18n keys in every locale; wiki updated.
A saved transit route is now its own reservation type instead of piggybacking
on train/bus: it gets a tram icon and its own color everywhere, and the day
timeline renders the itinerary inline — line badges in their official colors
with walk segments, plus the transfer count and walking time — instead of a
generic transport row.
Clicking the row opens the itinerary view (journey summary, stop-by-stop legs
with times, platforms, headsigns and operators) rather than the edit form;
editing stays reachable from an Edit action inside that view. The transit
type is registered across the timeline merge, transport modal, reservations
panel, file manager, map overlays and detail panels, with a translated type
label in every locale.
Each day header gets a transit button (replacing the rename pencil, which
moved next to the day name in the day detail panel). It opens a route search
backed by Transitous/MOTIS — free, open data, no paid provider: from/to stop
search with the day's own places as quick picks, depart/arrive time, mode
filters (train, subway, tram, bus, ferry, cable car) and ranking by best
route, fewer transfers or less walking. Results show local times, duration,
transfers, walking time and line badges in their official colors, with a
stop-by-stop breakdown per connection.
Adding a connection saves it as a regular transport reservation — typed by
its dominant leg, timed from the itinerary's wall-clock departure/arrival
converted to station-local time (tz-lookup), with the origin, transfer stops
and destination as endpoints and the compact legs in metadata.transit. It
slots into the day timeline by time and inherits editing, deletion and
drag-reordering from the existing transport machinery; the transport detail
view renders the full itinerary. Re-saving a transit transport through the
edit modal preserves the stored itinerary while the route is unchanged.
The server proxies the Transitous API (JWT-guarded, rate-limited, identifying
User-Agent, short response cache, strict mode whitelist); TRANSIT_API_URL
lets self-hosters use their own MOTIS instance. New i18n keys in every
locale, wiki page updated.
Discussion #1065
Adds two filter dropdowns next to the Search Expenses field (height-matched
to it): one filters by expense category, the other narrows to a single day.
Selecting a day shows a prominent summary banner with that day's total, and
hides the now-redundant per-day header. Both filters work on the desktop and
mobile layouts and compose with the existing search + all/mine/owed filters.
New i18n keys (costs.filter.allCategories / allDays, costs.expensesCount)
across every locale.
Trip invite links (#1143): each trip can have one rotating invite link in its
Share panel. An existing, logged-in user who opens /join/<token> is added to
the trip as a member; an anonymous visitor is sent to the login page and
returned to the invite afterwards — there is no registration from this link.
Reading, rotating or disabling the link all require the share_manage permission.
Admin invite trip binding (#1402): the admin create-invite dialog can now bind
a registration invite to a trip. Someone who registers via that link is
auto-added to the trip as a member (password and OIDC paths), inside the same
atomic step that consumes the invite.
Adds a trip_invite_tokens table and a nullable invite_tokens.trip_id, a shared
owner-safe/idempotent add-by-id helper, the manage + join endpoints, the
JoinTripPage and Share-panel section, the admin trip picker, and the new i18n
keys across every locale. Wiki updated.
Discussion #1143
Next to the existing "Open in Google Maps" action, add an OpenStreetMap button that
opens the place on openstreetmap.org (a marker at its coordinates, or a name search
when it has none) — the same map source TREK already renders, and a jumping-off point
for OSM-based apps like OrganicMaps / CoMaps. Requested in discussion #880.
Strings across all 22 locales; a unit test for the URL builder.
- #1394 planner: two transports on one day no longer draw a phantom airport→airport
road route between them (a run is only a drive when it holds a real place); mirrored
in the map hook and the sidebar's leg list, with a regression test.
- #1392 planner: the per-day Route button now points the selection at the tapped day
before toggling, so on mobile it computes that day's route instead of the previously
selected one, and only the selected day's button reads as active.
- #1372 planner: the "open in Google Maps" route now includes the day's hotel bookends,
matching the drawn map route.
- #1375 planner: a multi-day accommodation no longer thrashes the plan scroll — the
auto-scroll lock keys on the selection identity, not the per-day row.
- #1377 planner: the reset-orientation compass is now shown on small screens too.
- #1382 budget: settlement nets in the trip's canonical currency and converts to the
display currency once, so balances no longer drift with live FX and no phantom
third-party micro-flows appear (identity, hence unchanged, when they're the same).
- #1366 atlas: countries reached only by a transport booking (no lodging/place) now
count as visited, on both the dashboard and the Atlas page.
- #1383 bookings: a hotel linked to an accommodation shows only its day-range, not a
duplicate stamped date row, and the range stays correct after an edit.
- #1353 bookings: any non-hotel reservation can now link an existing trip place/activity.
- #1390 i18n: fix the Polish word for "buddies" (Towarzysze → Współpodróżnicy).
- #1265 planner: drag-and-drop of places now works on touch devices via a polyfill.
Port the three main-only fixes onto dev's (post-rewrite) architecture:
- fix(backups): prevent recursion when the backup path sits inside the backed-up dir
- fix(share): convert budget items to the viewer's base currency instead of a flat EUR
- fix(files): surface the descriptive server error for unsupported upload types (#1363)
Cherry-picked from 819aa793 on main; the SharedTripPage and useTripPlanner
conflicts were resolved to keep dev's font-scaling and full import set while
taking the fixes' currency conversion and translateApiError wiring.
- Replace fixed-order date parser with locale-aware implementation
using Intl.DateTimeFormat.formatToParts to detect field order;
adds swap fallback for unambiguous inputs (day > 12) to handle
locale mismatches gracefully
- Pre-fill keyboard input with locale-formatted numeric date
(e.g. 14.06.2026) instead of raw ISO value
- Replace all hardcoded English aria-labels and titles with t()
calls; add new keys under common.datepicker.* namespace across
all locale files
- Update FE-COMP-DATEPICKER-013 to use unambiguous day value (> 12)
to avoid locale-dependent test failures
Add a "Save to collection" action to the trip place list's selection bar (next to
bulk category + delete): it opens a list picker and copies every selected place
into the chosen list in one request, instead of one-by-one from each place.
- Server: saveFromTripPlaces (one access check + one WS notify), POST
places/from-trip-many; dedups by name/coords, skips missing ids, honours force.
- Client: saveFromTripMany api + SaveTripPlacesToListModal; the selection-bar
button is gated on the collections addon being enabled.
- Copy count / skipped-duplicates toast; strings in all 22 locales.
- Service + controller tests for the bulk path.
The owner now assigns each member a role — viewer (read + copy-to-trip only),
editor (default: add + edit places) or admin (full incl. delete). The owner is
always full. Existing members default to editor via migration 152, so nothing
regresses.
- Server: role column on collection_members (migration 152 + schema); roleOf +
assertCanEdit (save/update/status/list-meta) + assertCanDelete (delete) layered
on assertAccess; sendInvite takes a role; new setMemberRole (owner-only) +
POST members/role; members payload carries each role.
- Client: Share modal gains a role picker on invite and a per-member role select
for the owner (read-only role badge for others); the page hides add / edit /
status / move / delete for roles that can't perform them (server still enforces).
- Roles in all 22 locales; service + controller tests for the new gating.
- Trip "Saved places" picker: drop the fixed 360px cap so the list fills the
panel instead of stopping half-way, and add list + status filter dropdowns
(filter by which collection the place is saved in).
- "All saved" showed nothing on first open: setActive(ALL_SAVED) unioned the
lists from the store, but on first load those aren't fetched yet (loadAll still
running). Load them first when empty so the union isn't blank.
- The select toggle now sits at the right of the filter row (same height as the
status/category dropdowns) instead of the top toolbar.
- Select mode gains a "select all / deselect all" toggle and shows even with
nothing selected yet.
- Selected places can be moved or duplicated into another of your lists via a
target-list picker (move re-points collection_id; duplicate re-saves the place
data, carrying description / category / notes / etc.).
- New strings across all 22 locales.
- Rebuild the place detail as a clean, opaque, sectioned sheet (cover → meta →
status segment → description → links) with a proper footer action bar — the
loose "white lower half" is gone.
- Assign a place to a central (admin-defined) category, both in the detail edit
and when adding a place; categories are fetched once for the page.
- Add-place now sets category + description (markdown) + links + status in the
same step, closer to the trip's place form.
- Switching to the full-map view now closes the (list-docked) detail.
- Fix the selected-row highlight: it was clipped by the column's overflow — use
an inset ring and only clip during the map-collapse animation; a picked row
now scrolls into view above the detail sheet.
- New category strings across all 22 locales.
- Block the list-cover upload in demo mode (mirror the trips cover endpoint).
- Restrict list/place links to http(s) (schema) and normalise scheme-less URLs
to https:// on save, so a bare "booking.com" no longer resolves as a relative
SPA route (and javascript:/data: hrefs are rejected).
- Place detail: surface save errors with a toast instead of silently swallowing
a 400 and leaving the sheet stuck in edit mode.
- List editor: don't create a duplicate list when a retry follows a cover-upload
failure (reuse the created id); revoke the cover preview object URL.
- Map controls: one top bar (left toggle/select, right add/search) so they can't
overlap on a narrow split map — the search shrinks instead.
- Dashboard list badge: full-opacity colour wash so the name stays legible over
bright covers.
Dashboard widget (B): the collections tool now shows the user's LISTS as
compact colour-washed badges (cover image tinted with the list colour, or a
gradient) that jump to the list — one list() call, no N+1.
List details (C): lists gain a description, a custom cover image (uploaded to
/uploads/covers, tinted with the list colour in the hero) and links. A shared
ListEditorModal handles both create and edit; the hero shows the description +
link chips. New `links` JSON column on collections + collection_places
(migration 151) with parse/serialize in the service; a POST :id/cover upload
endpoint mirroring trips; cover-file cleanup path-confined locally.
Place detail (D): clicking a place opens a bottom sheet (no backdrop, so the
map stays visible) — status cycle, copy-to-trip, remove, and an edit mode with
a markdown description + links editor (collectionsApi.updatePlace, now wired
via a store action). A "+" next to the search adds a place to the list via the
maps search.
Fusion + fixes (E): the owner can now remove an accepted member (kick) — new
removeMember service/route/store + a button in ShareCollectionModal, with a
collections:removed WS bounce. findMembership no longer matches on name alone
(coordinate proximity required, killing "Starbucks everywhere" false positives).
loadCollection swallows a 403/404 after a leave/remove so the URL sync can't
throw uncaught. Grid remnants gone; the map select toggle moved onto the map.
New strings across all 22 locales; i18n parity strict passes.
- Drop the grid/tile view. The list view is now the default and, on wide
screens, a list + persistent map split; a top-left control on the map
collapses the list to a full-width map (and back), animating smoothly (the
map stays mounted and is nudged to re-layout during the transition). The
place search moves onto the map (top-right); mobile keeps a list/map toggle.
- Let a place be deselected again: clicking it once more, clicking the map
background, or picking another all toggle the selection (collections map now
wires onMapClick).
- Fix the stuck hover tooltip: selecting a place swaps its marker's DOM node so
the browser never fires mouseout/mouseleave, orphaning the fixed-position
tooltip (it hung on screen and drifted with scroll). Both map stacks now clear
the hover on selection change and on scroll.
- Remove CollectionGrid + PlaceCover; add hero eyebrow + map control strings.
Rebuilds the /collections page from the functional placeholder into the
dashboard's glass visual language (light + dark):
- A colour-washed hero per list: eyebrow + member avatars, big title, and
stat chips (All / Idea / Want / Visited) that double as the status filter.
- A sticky glass list rail (owned + shared + invites) with a mobile drawer.
- Gradient/photo cover place cards modelled on the trip cards, via a new
rectangular PlaceCover (photoService-backed, gradient fallback) + a shared
gradients util. List and map views restyled to match.
- Status pill rendered as a role=button span so it survives the .trek-dash
button reset and can nest inside the card; the share member-count badge is
now owner-only.
- New hero eyebrow strings across all locales.
- ShareCollectionModal: the owner manages a list's members and invites users
(available-users picker → invite, cancel pending); a member can leave a shared
list. The incoming accept/decline surface stays in the lists rail. A Share
button is added to the collections header for owners (and members, to reach
Leave).
- CollectionsWidget: a dashboard glass card after the currency widget showing the
saved count and the most recent saved places, double-gated by the admin addon
and a new per-user appearance flag.
- Appearance: a 'collections' dashboard-widget flag (desktop + mobile defaults)
wired into the appearance settings, surviving normalize.
- Sharing + settings strings across all 22 locales (parity strict passes).
Adds the client side of the Collections addon:
- A distinct /collections page (Atlas pattern, page/hook split) gated behind the
addon: a multi-list rail, a Grid (default) / List / Map view switch, the
idea/want/visited status with a one-tap badge, search and status filters, and
considered empty states. Store + hook + model + websocket wiring; the place
detail reuses the trip place inspector via a mode guard.
- Entry points: a "Save to Collection" button next to Open-in-Google-Maps in the
place inspector (and the two sidebar context menus), a "Copy to trip" modal,
and a desktop-only two-column add-place picker (mobile keeps the single-column
form).
- The collection namespace and the new keys across all 22 locales.
Adds the Collections addon backend: a server-wide-per-user library of saved
places, independent of any trip, with multiple named lists, an idea/want/visited
status, and Vacay-style fusion invitations to share a list with other users.
- Data: collection / collection_members / collection_places / collection_place_tags
tables (+ migration and baseline schema). Saved places carry the owner plus a
nullable saved_by so a member deleting their account can't drop shared content.
- Service: list + place CRUD with owner-or-accepted-member visibility, dedup,
status, save-from-trip and copy-to-trip (reusing the trip copy column list),
and the full fusion-invitation state machine mirrored from vacay (send / accept
/ decline / cancel / leave) with a websocket broadcast and an invite
notification. Deleting a list snapshots its members and notifies them.
- NestJS module + addon guard (404 before auth), registered in the app module.
- Widens the place photo cache reference check to count collection places so the
nightly sweep no longer evicts photos a saved place still uses.
- collection_invite notification wired across all 22 locales.
Rework the private-packing flag into a full sharing model. Every item is now
Common (the group pool — where all existing items live, so nothing breaks),
Personal (private to its owner) or Shared with specific people (it shows up on
those travelers' own lists, marked "by <bringer>"). is_private discriminates
restricted from common; a new packing_item_recipients table holds who a shared
item covers, and packing_item_contributors records "I can bring that too"
pledges on Common items.
The panel gains a Gemeinsam / Meine Liste view switch, each item a sharing
control (owner sets the tier + the people it covers), and Common items can be
co-brought or cloned onto your personal list. Visibility is enforced server-side
in listItems and the WebSocket broadcasts are scoped to exactly who can see an
item across every tier transition. All 22 locales stay in parity.
Add "guest" trip participants — people without a Trek account who can still be
assigned to costs, packing, to-dos and day-plan activities. A guest is a
credential-less users row (is_guest=1) joined into trip_members, so it is
assignable everywhere a real member is, with the cost-splitting, settlement,
packing and assignment paths working unchanged.
Guests are firewalled from everything account-related: they can never sign in
(password, OIDC and reset lookups skip them), never appear in the global user
directory, the member-add picker or admin user management, are never resolved as
notification recipients, can't be invited to another trip, and can't be made
owner. The trip owner manages guests from the share dialog in a dedicated,
clearly-labelled section (add / rename / remove), and guests carry a "Guest"
badge wherever members are picked. All 22 locales stay in parity.
Add POST /api/trips/:id/transfer so the owner can hand a trip to one of its
existing members. The swap runs in a transaction: the new owner takes
trips.user_id and the former owner is kept on as a regular member, so nobody
loses access. The endpoint is owner-only, writes a trip.transfer_ownership
audit entry and broadcasts the refreshed trip. The members modal gains a
"Make owner" action, shown only to the current owner.
Add drag-to-reorder to the packing and to-do lists, mirroring the budget
panel's native HTML5 drag pattern. A drag within a filtered/grouped view is
mapped back onto the global order so untouched items keep their place, and the
order persists optimistically via the existing reorder endpoints.
Packing items can now be marked private (#858): a private item is visible only
to its owner. createItem/bulkImport stamp the owner, listItems filters by the
viewer, and the WebSocket broadcasts are scoped to the owner so a private item
never reaches another member's screen — including the public/private toggle
transitions. Owners get a lock toggle and a private indicator on their items.
Bookings get a first-class url column (migration) instead of users pasting links into notes. It's editable in the booking modal and rendered as a clickable link on the reservation card. The reservation request schemas are open passthroughs, so only the entity schema + service SQL enumerate it.
Immich timeline and album listings no longer filter out videos; each asset now carries its media type, which the provider picker forwards when linking. A linked video streams through Immich's transcoded /video/playback endpoint, and the asset proxy forwards the viewer's Range header (and passes 206/Content-Range back) so the player can seek. Synology video stays excluded until its stream API is verified.
Adds media_type/media_types to the provider-photos request contract.
Update handlers accept an optional X-Base-Updated-At token and reject a stale overwrite with 409, returning the current server row. An absent token keeps the existing last-write-wins behaviour, so older clients are unaffected. packing_items gains an updated_at column (migration + stamped on every insert) so it can take part in conflict detection too.
Closes the UI half of #1168: in the Places selection mode, a new tag button
before delete opens a category picker that applies one category (or "No
category") to every selected place in a single request. Adds a REST
/places/bulk-update endpoint reusing updatePlacesMany, an offline-aware repo +
store action that patches both the place pool and the day-assignment
projections, undo grouped by each place's prior category, and the i18n keys
across all locales.
Add the keys dev gained since this PR opened so the new vi locale keeps full
parity: the help namespace (wiki help center), settings appearance options,
costs split modes, dashboard Unsplash cover search, the insecure-cookie login
hint, nav.help and the admin group labels.
Add per-payer and per-member custom split amounts with Equally, Custom and
Ticket split modes on top of the existing equal split, keep legacy "paid by"
expenses working, and document the modes in the Budget Tracking wiki page.
When the server issues a Secure session cookie but the request arrived over
plain HTTP (the common LAN install over http://ip:3000), the browser drops
the cookie and the next request dead-ends on a bare "Access token required" —
the top source of avoidable install issues. The login response now flags this
exact case and the login page shows a localized box explaining the fix (use
HTTPS, or set COOKIE_SECURE=false) with a link to the Troubleshooting guide.
It only triggers in the real failure case, never for correct HTTPS setups.
Add a Help section (profile menu, /help) that renders the GitHub wiki inside
TREK. /api/help fetches the wiki markdown — the nav from _Sidebar.md, pages,
and proxied images — from GitHub and caches it (1h TTL, serves stale on
outage), so it auto-syncs on wiki edits with no redeploy and the client never
calls GitHub directly. The page is styled to match TREK with a section
sidebar, search and react-markdown; wiki [[links]] are rewritten to in-app
routes and HTML-comment placeholders are stripped. Page state lives in a
useHelp() hook per the page pattern. Adds nav.help and a help namespace
across all locales.
The admin sidebar had 11 flat tabs. PageSidebar now supports optional group headings (backward-compatible; the Settings sidebar stays flat), and the admin tabs are grouped into Users, Configuration, Integrations and Maintenance. Group labels added across all locales.
The Display tab became a catch-all once theming moved to its own Appearance tab, and its 'Display' label no longer fit. It is now 'General' (Allgemein) and split into 'Language & region' and 'Travel & map' sections. Tab labels and the new section titles are added across all locales.
Transparency-off, density and per-size typography are best-effort while the token migration is ongoing, so the section carries an Experimental badge. Adds the i18n key across all locales.
The four size-class sliders (Large/Medium/Normal/Small) are now always visible instead of behind a disclosure, each with a live sample rendered at that size and an example of what it affects (e.g. Normal = place names/descriptions, Small = addresses/labels).
The text-size control now adjusts each size class (Large / Medium / Normal / Small) independently as well as all-at-once. Inline px sizes are mapped to a class by their value, so the per-class sliders reach real content; each class variable = global factor x its per-class factor (no double-scaling with the root font-size that handles rem text). The settings UI gains a live preview that resizes as you drag, and the four size sliders sit behind a clear toggle.
Add the new appearance settings keys (widget group titles, sidebar/density hints) to every locale so the strict key-parity check passes, and update the single-emoji chat test to expect the now-scalable calc() font size.
Dashboard widget settings are grouped by where they sit on the dashboard (below the hero / right sidebar / bottom of page); the right-sidebar master toggle now nests its individual widgets and greys them out when the sidebar is off, instead of a confusing flat list mixing the master with its children. Density gains an explanatory hint plus a real compact spacing effect. Transparency-off also solidifies the Atlas glass panels and tooltip, Leaflet zoom controls and GL popups — class-based surfaces via CSS, the Atlas inline panels via a noTransparency flag.