From ad2119efd6d8d22ff3fbaa7d1d386ca1ffdda515 Mon Sep 17 00:00:00 2001 From: Maurice <61554723+mauriceboe@users.noreply.github.com> Date: Tue, 25 Aug 2026 15:22:46 +0200 Subject: [PATCH] docs(wiki): correct what reading each page whole turned up (#2036) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The last pass reviewed diffs, so it never saw a new paragraph contradicting an older one on the same page, or a long-standing claim nobody had checked. Plugin authors were told ctx.config carries their per-user secrets and that definePlugin takes a permissions key — neither is true, and both fail silently. FORCE_HTTPS was said to need TRUST_PROXY, which it does not. --- wiki/AI-Booking-Import.md | 4 +-- wiki/Accommodations.md | 2 +- wiki/Admin-MCP-Tokens.md | 2 +- wiki/Admin-Users-and-Invites.md | 2 +- wiki/Atlas.md | 2 +- wiki/Audit-Log.md | 2 +- wiki/Collections.md | 4 +-- wiki/Creating-a-Trip.md | 8 ++--- wiki/Dashboard-Widgets.md | 4 +-- wiki/Day-Plans-and-Notes.md | 2 +- wiki/Display-Settings.md | 2 +- wiki/Documents-and-Files.md | 8 ++--- wiki/Environment-Variables.md | 15 +++++--- wiki/Home.md | 2 +- wiki/Install-Docker-Compose.md | 2 +- wiki/Install-Portainer.md | 2 +- wiki/Install-Proxmox.md | 1 - wiki/Invite-Links.md | 2 +- wiki/Journey-Journal.md | 2 +- wiki/Login-and-Registration.md | 4 +-- wiki/MCP-Addon-Tools.md | 2 +- wiki/MCP-Tools-and-Resources.md | 4 +-- wiki/Map-Features.md | 2 +- wiki/Map-Settings.md | 6 ++-- wiki/My-Trips-Dashboard.md | 4 +-- wiki/Offline-Mode-and-PWA.md | 6 ++-- wiki/PDF-Export.md | 2 +- wiki/Packing-Lists.md | 4 +-- wiki/Packing-Templates.md | 2 +- wiki/Password-Reset.md | 2 +- wiki/Places-and-Search.md | 4 +-- wiki/Plugin-Cookbook.md | 14 ++++---- wiki/Plugin-Development.md | 27 ++++++++------- wiki/Plugin-Permissions.md | 16 +++++---- wiki/Plugins.md | 13 ++++--- wiki/Public-Share-Links.md | 4 +-- wiki/Reservations-and-Bookings.md | 4 +-- wiki/Reverse-Proxy.md | 15 +++++--- wiki/Security-Hardening.md | 4 +-- wiki/Tags-and-Categories.md | 14 +++++--- wiki/Todos-and-Tasks.md | 2 +- wiki/Transport-Flights-Trains-Cars.md | 13 ++++--- wiki/Trip-Members-and-Sharing.md | 4 +-- wiki/Trip-Planner-Overview.md | 2 +- wiki/Troubleshooting.md | 50 ++++++++++++++++++++------- wiki/Weather-Forecasts.md | 2 +- 46 files changed, 175 insertions(+), 119 deletions(-) diff --git a/wiki/AI-Booking-Import.md b/wiki/AI-Booking-Import.md index ed5b0388b..f0a2725d7 100644 --- a/wiki/AI-Booking-Import.md +++ b/wiki/AI-Booking-Import.md @@ -12,7 +12,7 @@ AI parsing does not replace [KDE Itinerary](Reservations-and-Bookings) — it ba 1. Every uploaded file is parsed by KDE Itinerary first. 2. Only files that Itinerary returns **nothing** for are sent to the AI model. -3. Every reservation the AI produces is flagged **Review** so you can confirm it before (or after) saving. +3. Because LLM extraction is less certain, every reservation the AI produces is marked for review internally, and the import walks you through the normal editor item by item to confirm and correct it before it is saved. The mark never becomes a visible badge: the review editor saves each booking as reviewed, so a saved AI import carries no **Review** flag. So structured tickets keep being parsed the fast, deterministic way; the AI only steps in for the documents that would otherwise fail. If the addon is disabled, import behaves exactly as before. On an install without the extractor there is nothing to back up — every file goes straight to the model. @@ -83,7 +83,7 @@ It is a retry, not a different route — on a normal import the model has alread The model is asked to capture the full booking — including **every leg of a multi-segment flight** — and, on save, TREK wires each item into the trip: - **Fields** — booking/confirmation code, dates and times, and per type: seat, class, platform, total price and currency; hotels bring their address, rental cars their company, restaurants and events their venue with phone and website. -- **Places** — hotel, restaurant and event venues (and un-geocoded transport stops) are geocoded and added as trip places, so the map pin appears. +- **Places** — only a **hotel** booking becomes a trip place. On save the review editor reuses an existing trip place whose name matches; otherwise the reviewed address is geocoded and a new place is created and linked, so the map pin appears. Transport stops are geocoded while the files are parsed, but they are stored on the booking as its **From → To endpoints**, not as trip places — a stop that could not be located is listed as a warning on the parse result and is dropped when you save, so fix it in the review editor first. Restaurant and event venues arrive only as the booking's location text: the editor can link a place that already exists in the trip, but it never creates one for you. - **Accommodations** — a hotel booking creates the accommodation on the matching check-in/check-out days. - **Linked cost** — if the [Costs/Budget addon](Budget-Tracking) is enabled and the booking has a price, a linked expense is created. Without that addon, the price stays on the reservation only. - **Source document** — the uploaded file is attached to the reservation's files. diff --git a/wiki/Accommodations.md b/wiki/Accommodations.md index 49f681153..f617e333e 100644 --- a/wiki/Accommodations.md +++ b/wiki/Accommodations.md @@ -6,7 +6,7 @@ Link an accommodation to specific check-in and check-out days so it appears for There are two ways to create an accommodation: -**From the Reservations panel:** Click **Add** and select **Hotel** as the booking type. When the type is set to Hotel, the date/time and location fields are replaced by accommodation-specific inputs (see below). Saving the form creates both the hotel reservation and the underlying accommodation record at the same time. +**From the Reservations panel:** Click **Manual Booking** and select **Hotel** as the booking type. When the type is set to Hotel, the date/time and location fields are replaced by accommodation-specific inputs (see below). Saving the form creates both the hotel reservation and the underlying accommodation record at the same time. **From the Day Detail panel:** Click the hotel icon or the **Add accommodation** button in the Day Detail overlay. A picker appears that lets you select a place from the trip, choose the day range, and optionally fill in check-in/check-out times and a confirmation code. This creates the accommodation record and its linked Hotel reservation together. diff --git a/wiki/Admin-MCP-Tokens.md b/wiki/Admin-MCP-Tokens.md index 2db0004ad..c3f7f4caf 100644 --- a/wiki/Admin-MCP-Tokens.md +++ b/wiki/Admin-MCP-Tokens.md @@ -14,7 +14,7 @@ OAuth sessions are created when a user authorizes an MCP client via the OAuth 2. | Column | Description | |--------|-------------| -| Client name | The name of the registered OAuth client. Granted scopes are shown as badges below the name (up to 6 are shown; click "+N more" to expand) | +| Client | The name of the registered OAuth client. Granted scopes are shown as badges below the name (up to 6 are shown; click "+N more" to expand) | | Owner | The username of the user who authorized the session | | Created | Date the session was established | | (actions) | Trash icon button to revoke | diff --git a/wiki/Admin-Users-and-Invites.md b/wiki/Admin-Users-and-Invites.md index 570de54b8..ef57dbe9a 100644 --- a/wiki/Admin-Users-and-Invites.md +++ b/wiki/Admin-Users-and-Invites.md @@ -28,7 +28,7 @@ Click the pencil icon on any row to open the edit form. You can change: - **Username** - **Email address** - **Role** — a dropdown with **User** and **Administrator** -- **Password** — set a new password; must be at least 8 characters +- **Password** — set a new password. It must be at least 8 characters and contain an uppercase letter, a lowercase letter, a number and a special character; commonly used passwords and strings made of a single repeated character are rejected. The same rules apply to the password you set under [Creating a user directly](#creating-a-user-directly). See [Login and Registration](Login-and-Registration#password-requirements). Click **Save** to apply changes. diff --git a/wiki/Atlas.md b/wiki/Atlas.md index b4e120b0f..d2543fd5e 100644 --- a/wiki/Atlas.md +++ b/wiki/Atlas.md @@ -51,7 +51,7 @@ Your Atlas statistics panel shows: - **Countries visited** — total number of distinct countries you have actually been to. Countries from upcoming trips are counted separately and shown next to this number. - **Trips** — total number of trips across all time. - **Places** — total number of individual places logged in trips. -- **Cities** — total number of distinct cities visited. Derived from the addresses of your places, read from the administrative end of the address and cross-checked against the region the map resolved for that place, so a restaurant or a street is never mistaken for a city. +- **Cities** — total number of distinct cities visited, derived from the addresses of your places. TREK drops the last comma-separated part (the country), then walks back through the remaining parts and takes the first one that is still non-empty once digits, hyphens and postal marks are stripped, lower-cased so spelling variants collapse into a single entry. This is a heuristic over a formatted address string rather than a lookup, so the figure is approximate: a short address such as `Osteria Francescana, Italy` leaves nothing but the place's own name, and an address whose administrative tail ends on a state or prefecture (`…, Shibuya, Tokyo, 150-0002, Japan`) counts that region rather than the city. - **Travel days** — total days spent travelling. - **Continent breakdown** — number of countries visited per continent (Europe, Asia, North America, South America, Africa, Oceania). Antarctica joins the row once you have been. - **Travel streak** — number of consecutive years in which you have taken at least one trip. diff --git a/wiki/Audit-Log.md b/wiki/Audit-Log.md index bb861a4e8..e12890d97 100644 --- a/wiki/Audit-Log.md +++ b/wiki/Audit-Log.md @@ -155,7 +155,7 @@ The panel loads 100 entries at a time by default. Click **Load more** at the bot The client IP is the one Express resolves after applying `TRUST_PROXY`, not whatever the `X-Forwarded-For` header happens to say. That distinction matters: the header is written by the caller, and an audit row an attacker can address to someone else's IP is worse than no row at all. -`TRUST_PROXY` is **the number of proxy hops in front of TREK**, and it has to be accurate. With `TRUST_PROXY=1` (the default) TREK trusts exactly one hop, so a request that passed through two proxies is recorded as coming from the *inner* one. If your setup is Cloudflare in front of nginx in front of TREK, set `TRUST_PROXY=2`. Set it to `0` to trust nothing and always record the socket address. +`TRUST_PROXY` is **the number of proxy hops in front of TREK**, and it has to be accurate. With `TRUST_PROXY=1` (the default) TREK trusts exactly one hop, so a request that passed through two proxies is recorded as coming from the *outer* one — the Cloudflare edge in the example below, not nginx and not the real client. If your setup is Cloudflare in front of nginx in front of TREK, set `TRUST_PROXY=2`. Set it to `0` to trust nothing and always record the socket address. See [Environment-Variables](Environment-Variables). diff --git a/wiki/Collections.md b/wiki/Collections.md index 13cd0be53..e1f57b305 100644 --- a/wiki/Collections.md +++ b/wiki/Collections.md @@ -1,6 +1,6 @@ # Collections -Collections is a personal, server-wide library of saved places that lives outside of any single trip. Keep multiple named lists of places you have discovered — a "Norway road trip" wishlist, "Best coffee in Lisbon", "Someday" — each with a want-to-go / visited status, and share a list with other users. +Collections is a personal, server-wide library of saved places that lives outside of any single trip. Keep multiple named lists of places you have discovered — a "Norway road trip" wishlist, "Best coffee in Lisbon", "Someday" — each place carrying an idea / want-to-go / visited status, and share a list with other users. > **Admin:** enable Collections in [Admin-Addons](Admin-Addons). @@ -90,7 +90,7 @@ When sharing, the owner assigns each member a permission role, and can change it | Role | Can do | |---|---| -| **Viewer** | View the list and copy its places into their own trips, and filter by label — no changes to the list. | +| **Viewer** | View the list, cast their own star rating on its places, copy its places into their own trips, and filter by label — no other changes to the list. | | **Editor** *(default)* | Add new places and edit existing ones, and manage + assign the list's labels. | | **Admin** | Everything an editor can, plus delete places. | diff --git a/wiki/Creating-a-Trip.md b/wiki/Creating-a-Trip.md index a97d7a580..51b0315c0 100644 --- a/wiki/Creating-a-Trip.md +++ b/wiki/Creating-a-Trip.md @@ -16,15 +16,15 @@ The trip name. Cannot be empty — saving is blocked until a title is entered. ### Description (optional) -A short free-text description shown on the trip card. +A short free-text description. It is not shown on the trip card or on the Spotlight card. It appears on a [public share page](Public-Share-Links), on the cover of a [PDF export](PDF-Export), and as the notes of the trip's all-day event in [calendar feeds and .ics exports](Calendar-Feeds). ### Dates Set a **Start date** and **End date** using the date picker. The day count is calculated automatically when both are set. -If you leave **both** dates empty, a separate **Day count** field appears. Enter a number between **1 and 365** to create a date-less itinerary with a fixed number of days. +On desktop, if you leave **both** dates empty, a separate **Day count** field appears. Enter a number between **1 and 365** to create a date-less itinerary with a fixed number of days. The mobile sheet has no such field: a date-less trip created there always starts with 7 days. -You cannot set only one date and leave the other blank via normal interaction — setting a start date auto-fills or adjusts the end date to preserve the previous duration. +Only the **start** date is linked to the other one. Picking a start date fills the end date to match it, and moving the start date of an already-dated trip shifts the end date so the previous duration is kept. Nothing links them the other way round: you can pick an end date on its own, and the picker's **✕** button clears either field afterwards. A trip can therefore be saved with both dates, with only a start date, with only an end date, or with neither. A trip left with exactly one date gets an undated day grid (7 days by default), just like a date-less one. ### Currency @@ -36,7 +36,7 @@ Pick the currency you will actually settle up in. It is not a cosmetic label, bu ### Cover Image -The cover image is displayed on the trip card and as the background of the Spotlight card. You can add one in three ways: +The cover image is displayed on the trip card and as the background of the Spotlight card. You can add one in four ways: - **Drag and drop** an image file onto the dashed upload area. - **Paste from clipboard** — if you have an image in your clipboard, paste it anywhere in the dialog. diff --git a/wiki/Dashboard-Widgets.md b/wiki/Dashboard-Widgets.md index 1188b6255..acede69e2 100644 --- a/wiki/Dashboard-Widgets.md +++ b/wiki/Dashboard-Widgets.md @@ -31,11 +31,11 @@ The currency converter lets you quickly convert an amount between two currencies 1. Enter an amount in the input field. 2. Select a source currency from the left selector. 3. Select a target currency from the right selector. -4. The converted amount is displayed immediately below. +4. The converted amount appears in the right-hand ("To") field, directly opposite the amount you entered and above the target-currency selector — shown to two decimals, without a currency symbol. Below the row, the current rate is printed as `1 EUR = 1.0850 USD`; if no rate could be fetched, the field shows `—` and the line reads "Rate unavailable". You can also click the swap arrow to reverse source and target. -**Exchange rates** are fetched from [Frankfurter](https://frankfurter.dev) using the `https://api.frankfurter.dev/v2/rates?base={from}` endpoint. Rates are refreshed each time you change a currency or click the refresh icon. +**Exchange rates** are fetched from [Frankfurter](https://frankfurter.dev) using the `https://api.frankfurter.dev/v2/rates?base={from}` endpoint. One request returns every quote for the source currency, so rates are re-fetched when you change the source currency (including via the swap arrow) or click the refresh icon; changing only the target currency re-uses the rates already loaded. **Supported currencies:** 165 currencies are available in the selector — the full set Frankfurter v2 supports, including all major fiat currencies (USD, EUR, GBP, JPY, etc.) and many minor ones. diff --git a/wiki/Day-Plans-and-Notes.md b/wiki/Day-Plans-and-Notes.md index 4ecdf9a51..decd1ced1 100644 --- a/wiki/Day-Plans-and-Notes.md +++ b/wiki/Day-Plans-and-Notes.md @@ -24,7 +24,7 @@ Items are sorted by their time or position index. ![Adding a place by dragging](assets/DayItineraryAddPlaceDragging.gif) -- **Add button** — Click on the day and then click the **+** button inside an expanded day section to open an inline search panel; find the place and tap it to assign. +- **Add button** — click a day header to select it, then click the **+** on a place in the right-hand Places sidebar; the place is assigned to the selected day straight away. ![Adding a place by button](assets/DayItineraryAddPlaceByButton.gif) diff --git a/wiki/Display-Settings.md b/wiki/Display-Settings.md index 656a0c1bb..aea399351 100644 --- a/wiki/Display-Settings.md +++ b/wiki/Display-Settings.md @@ -67,7 +67,7 @@ Your **display currency** — the currency you want to *read* amounts in on the > This is **not** the trip's currency, which is set on the trip itself and is the base its balances are calculated in. The distinction matters — see [Currencies](Currencies). -An administrator can set the instance-wide default for new users in Admin → Default User Settings. Choosing **Trip currency** yourself overrides it. +An administrator can set an instance-wide default in Admin → Default User Settings. It is not only a starting value for new accounts: it is merged in every time your settings are loaded, so it applies to anyone whose own display currency is empty. Picking a specific currency of your own overrides it. **Trip currency** does not, because it stores an empty value that counts as "not set", so the admin default takes effect again on your next reload. ## Language diff --git a/wiki/Documents-and-Files.md b/wiki/Documents-and-Files.md index 712200686..790acc755 100644 --- a/wiki/Documents-and-Files.md +++ b/wiki/Documents-and-Files.md @@ -28,12 +28,12 @@ The toolbar provides filter tabs, in this order: **All**, a star icon for **Star ## Previewing files -Clicking a non-image file (e.g., PDF) opens an inline preview modal with options to open in a new tab or download. Clicking an image file opens a full-screen lightbox. You can: +Clicking a file that is neither an image nor a video (e.g., PDF or Markdown) opens an inline preview modal with options to open in a new tab or download. `.pkpass` and `.pkpasses` Wallet passes are the exception: clicking one downloads it straight away, so the OS can hand it to Apple Wallet rather than forcing it into the in-app preview. Clicking an image or video file opens a full-screen lightbox. You can: -- Navigate between images using the **arrow buttons** or the **left/right arrow keys**. +- Navigate between images and videos using the **arrow buttons** or the **left/right arrow keys**. - Swipe left/right on touch devices. -- Jump to a specific image using the **thumbnail strip** at the bottom. -- Download or open the image in a new tab from the lightbox header. +- Jump to a specific file using the **thumbnail strip** at the bottom. Videos have no stored thumbnail and show a play placeholder there. +- Download or open the file in a new tab from the lightbox header. ## Starring diff --git a/wiki/Environment-Variables.md b/wiki/Environment-Variables.md index 7c1630069..5dff7acc4 100644 --- a/wiki/Environment-Variables.md +++ b/wiki/Environment-Variables.md @@ -55,7 +55,7 @@ like `NODE_ENV=staging` still boots. | `ALLOWED_ORIGINS` | Comma-separated origins for CORS and email notification links | same-origin | | `ALLOW_INTERNAL_NETWORK` | Allow outbound requests to private/RFC-1918 IPs. Set `true` if Immich or other integrated services are on your local network. Loopback (`127.x`) and link-local (`169.254.x`) addresses remain blocked regardless. | `false` | | `APP_URL` | Public base URL (e.g. `https://trek.example.com`). Required when OIDC is enabled — must match the redirect URI registered with your IdP. Also used as the base URL for email notification links and subscribable calendar feed URLs (the `webcal://`/`https://` links the Subscribe dialog hands to Google/Apple/Outlook). | — | -| `TREK_WIKI_DIR` | Where the in-app Help pages (`/help`) read their content from. TREK ships this wiki and serves it from disk, so the docs always match the version you are running. You should not need to set this — it is an escape hatch for unusual layouts. If the directory cannot be found, Help falls back to fetching the public GitHub wiki (which tracks the latest release and needs outbound network access). | the bundled `wiki/` directory | +| `TREK_WIKI_DIR` | Where the in-app Help pages (`/help`) read their content from. TREK ships this wiki and serves it from disk, so the docs always match the version you are running. You should not need to set this — it is an escape hatch for unusual layouts. If the directory cannot be found, Help falls back to fetching the repository's `wiki/` folder from the `main` branch on GitHub (which can be ahead of the release you are running, and needs outbound network access). | the bundled `wiki/` directory | ### `HOST` — Source and Proxmox installs only @@ -156,12 +156,17 @@ full explanation. | Variable | Description | Default | |---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------| -| `FORCE_HTTPS` | When `true`: 301-redirects HTTP→HTTPS, sends HSTS (`max-age=31536000`), adds CSP `upgrade-insecure-requests`, forces cookie `secure` flag. Only useful behind a TLS proxy. Requires `TRUST_PROXY`. | `false` | +| `FORCE_HTTPS` | When `true`: 301-redirects HTTP→HTTPS, sends HSTS (`max-age=31536000`), adds CSP `upgrade-insecure-requests`, forces cookie `secure` flag. Only useful behind a TLS proxy. Your proxy must send `X-Forwarded-Proto: https`. | `false` | | `HSTS_INCLUDE_SUBDOMAINS` | When `true`: adds the `includeSubDomains` directive to the HSTS header, extending HTTPS enforcement to all subdomains. Only effective when HSTS is active (`FORCE_HTTPS=true` or `NODE_ENV=production`). Leave `false` if you run other services on sibling subdomains over plain HTTP. | `false` | -| `TRUST_PROXY` | Number of trusted proxy hops. Tells Express how far into `X-Forwarded-For` to look for the real client IP, and where to read `X-Forwarded-Proto`. Count your hops: with two proxies in front of TREK and `TRUST_PROXY=1`, the IP on every audit row is the inner proxy's. `0` trusts nothing and always uses the socket address. Required for `FORCE_HTTPS` to detect the forwarded protocol. | `1` (production) | +| `TRUST_PROXY` | Number of trusted proxy hops. Tells Express how far into `X-Forwarded-For` to look for the real client IP, and where to read `X-Forwarded-Proto`. Count your hops: with two proxies in front of TREK and `TRUST_PROXY=1`, the IP on every audit row is the inner proxy's. `0` trusts nothing and always uses the socket address. Not required for the `FORCE_HTTPS` redirect, which reads the `X-Forwarded-Proto` header directly — set it for correct client IPs in the audit log, and for the `COOKIE_SECURE` auto-derivation, which goes through `req.secure`. | `1` (production) | | `COOKIE_SECURE` | Controls the `secure` flag on the `trek_session` cookie. Auto-derived as `true` when `NODE_ENV=production`, when `FORCE_HTTPS=true`, or when the request itself arrived over TLS on the outermost hop (`X-Forwarded-Proto: https` with `TRUST_PROXY` set). Set to `false` only as an escape hatch for LAN testing without TLS — not recommended in production. | auto | -> **Warning:** `FORCE_HTTPS=true` without `TRUST_PROXY` set causes a redirect loop. +> **Warning:** `FORCE_HTTPS=true` behind a proxy that does not forward `X-Forwarded-Proto: https` causes a +> redirect loop — every request looks like plain HTTP to TREK and gets 301'd again. Fix the proxy to send the +> header. Setting `TRUST_PROXY` does not help: the redirect accepts the raw `X-Forwarded-Proto: https` header on +> its own, whatever `trust proxy` is set to. (`TRUST_PROXY` only feeds the second half of the check, `req.secure`, +> which additionally covers a proxy chain that sends a comma-joined `X-Forwarded-Proto` — and production already +> trusts one hop by default.) --- @@ -387,6 +392,8 @@ The plugin system is **on by default**. The runtime and the Admin → Plugins pa | `TREK_PLUGIN_RPC_PER_SEC` | Sustained rate limit (calls **per second**) for a plugin's host RPC calls (`ctx.*`) once its burst allowance is spent. Prevents a tight-loop plugin from starving the single-threaded host. | `20` | | `TREK_PLUGIN_RPC_BURST` | Burst allowance — how many host RPC calls a plugin may fire back-to-back before the per-second limit applies. | `60` | | `TREK_PLUGIN_RPC_INFLIGHT` | Max concurrent host→plugin RPC dispatches allowed for a single plugin at once (concurrency cap). | `16` | +| `TREK_PLUGIN_LOG_PER_SEC` | Sustained rate limit (log lines **per second**) for a plugin's log output — `ctx.log.*`, the child process's stdout/stderr, and unknown plugin event topics — once its burst allowance is spent. This path is **not** covered by the RPC limiter above, and `warn`/`error` lines land in a synchronous `plugin_error_log` INSERT + prune on the host thread, so this is what stops a `while (true) ctx.log.error(...)` loop from pinning the instance. Excess lines are dropped; when logging resumes, one `warn` line reports how many were dropped, so the throttling is visible to an operator. | `10` | +| `TREK_PLUGIN_LOG_BURST` | Burst allowance — how many log lines a plugin may emit back-to-back before the per-second limit applies. | `50` | | `TREK_PLUGIN_AUDIT_MAX_ROWS` | Per-plugin retention cap for the capability audit log (kept in the shared `data/travel.db`). The newest N rows per plugin are retained and older ones pruned; the retained window stays tamper-evident. Set to `0` to disable pruning. | `20000` | All of these are optional — the defaults are safe. Set `TREK_PLUGINS_ENABLED=false` if you want to switch the plugin system off entirely. diff --git a/wiki/Home.md b/wiki/Home.md index 9b44865d2..c0f692642 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -24,7 +24,7 @@ TREK is a self-hosted, real-time collaborative travel planner licensed under AGP ### Collaboration - **Real-Time Sync** — WebSocket-based live sync; changes appear instantly for all connected users - **Multi-User** — invite members with role-based access -- **Invite Links** — one-time registration links with configurable max uses and expiry +- **Invite Links** — registration links with configurable max uses (1–5 or unlimited) and expiry - **OIDC SSO** — sign in with Google, Apple, Authentik, Keycloak, or any OIDC provider - **Two-Factor Authentication** — TOTP-based 2FA with QR code setup - **Passkeys** _(admin-enabled)_ — passwordless WebAuthn sign-in with Touch ID, Windows Hello, Android screen lock, or a hardware key; owning one also satisfies the require-2FA policy (see [Passkeys](Passkeys)) diff --git a/wiki/Install-Docker-Compose.md b/wiki/Install-Docker-Compose.md index 1176d61ec..ca6cc1c54 100644 --- a/wiki/Install-Docker-Compose.md +++ b/wiki/Install-Docker-Compose.md @@ -12,7 +12,7 @@ The compose file ships with several hardening options enabled by default: | Setting | What it does | |---|---| -| `read_only: true` | Mounts the container filesystem read-only; only the two named volumes and `/tmp` are writable | +| `read_only: true` | Mounts the container filesystem read-only; only the two mounted volumes (`/app/data`, `/app/uploads`) and `/tmp` are writable | | `security_opt: no-new-privileges:true` | Prevents the process from gaining additional Linux privileges via setuid/setgid executables | | `cap_drop: [ALL]` | Drops all Linux capabilities from the container | | `cap_add: [CHOWN, SETUID, SETGID]` | Adds back only the capabilities needed for the entrypoint to drop privileges to the `node` user | diff --git a/wiki/Install-Portainer.md b/wiki/Install-Portainer.md index 480835d09..4af93c8a4 100644 --- a/wiki/Install-Portainer.md +++ b/wiki/Install-Portainer.md @@ -64,7 +64,7 @@ How you update depends on the tag you chose: ![Edit stack page with an arrow pointing to the Update the stack button](assets/portainer-update-stack.png) -> Back up your data before any update. Go to **Admin Panel → Backups** or copy your `./data` and `./uploads` directories. See [Backups](Backups). +> Back up your data before any update. Go to **Admin Panel → Backup** or copy your `./data` and `./uploads` directories. See [Backups](Backups). ## Volumes diff --git a/wiki/Install-Proxmox.md b/wiki/Install-Proxmox.md index 158472ccc..9f0acb339 100644 --- a/wiki/Install-Proxmox.md +++ b/wiki/Install-Proxmox.md @@ -84,7 +84,6 @@ If your Proxmox host has multiple network interfaces and you want TREK to listen ``` HOST=10.0.0.72 # bind only on this LAN interface -PORT=3001 ``` > **Note:** `HOST` is only relevant for source-based and Proxmox installs. Do not use it in Docker or any containerised deployment. diff --git a/wiki/Invite-Links.md b/wiki/Invite-Links.md index 7ed023ad7..3ac04405c 100644 --- a/wiki/Invite-Links.md +++ b/wiki/Invite-Links.md @@ -20,7 +20,7 @@ When creating an invite link you set two parameters: **Expiry** — how long until the link stops working. Choose from preset buttons: **1d, 3d, 7d, 14d**, or **∞** (no expiry). -**Add to trip (optional)** — bind the invite to one of your trips. When someone registers through the link, they are automatically added to that trip as a member. Leave it on **No trip** for a plain registration invite. The selector only appears when at least one trip exists. +**Add to trip (optional)** — bind the invite to any trip on the instance, not just your own. When someone registers through the link, they are automatically added to that trip as a member. Leave it on **No trip** for a plain registration invite. The selector only appears when at least one trip exists. Once created, a 32-character hexadecimal token is generated and the URL is automatically copied to your clipboard. diff --git a/wiki/Journey-Journal.md b/wiki/Journey-Journal.md index 4d1759990..ab7789402 100644 --- a/wiki/Journey-Journal.md +++ b/wiki/Journey-Journal.md @@ -43,7 +43,7 @@ Each entry corresponds to a day in your journey. The entry editor provides: ### Getting around a long journal -Everything that adds to a journal sits at the top of the page — the **Add Entry** button, the gallery upload — while what you were last reading sits at the bottom. Two small buttons ride along the left edge of the timeline once there is more than a screenful to travel: one jumps back to the top, the other to the last entry. Each appears only when there is somewhere to go in that direction. +Everything that adds to a journal sits at the top of the page — the **Add Entry** button, the gallery upload — while what you were last reading sits at the bottom. On desktop, two small round buttons float centred over the timeline, just above its bottom edge, once there is more than 400 px of scroll to travel: one jumps back to the top, the other to the last entry. Each appears only when there is somewhere to go in that direction. ### External photos diff --git a/wiki/Login-and-Registration.md b/wiki/Login-and-Registration.md index 925f0bb0e..c19585534 100644 --- a/wiki/Login-and-Registration.md +++ b/wiki/Login-and-Registration.md @@ -21,7 +21,7 @@ The **Remember me** switch below the password field decides how long the session Both durations are configurable per instance, see [Environment-Variables](Environment-Variables). Your choice is carried through the MFA step, through a password change, and through SSO sign-in. While you keep using TREK the cookie is silently re-issued once the token is past half its lifetime, with the same Remember me semantics, so an active session never expires mid-use. Only cookie sessions are renewed this way — API and MCP clients authenticating with a bearer token are not. -Sign-ins that never pass through this switch — registration, the **"Try demo"** button and passkey sign-in — get the historical default instead: a persistent cookie lasting `SESSION_DURATION`. +Sign-ins that never pass through this switch — registration, the demo button and passkey sign-in — get the historical default instead: a persistent cookie lasting `SESSION_DURATION`. ### Forced password change @@ -89,7 +89,7 @@ Forgot-password requests are rate-limited to **3 attempts per 15-minute window** ## Demo mode -When the server is started with `DEMO_MODE=true`, a **"Try demo"** button appears below the login form. Clicking it signs you in as the demo user without entering credentials. The demo credentials (`demo@trek.app` / `demo12345`) are also displayed in the app config for reference, but the one-click button is the intended entry point. +When the server is started with `DEMO_MODE=true`, a **"Try the demo — no registration needed"** button appears below the login form. Clicking it signs you in as the demo user without entering credentials. The demo credentials (`demo@trek.app` / `demo12345`) are also displayed in the app config for reference, but the one-click button is the intended entry point. ## SSO diff --git a/wiki/MCP-Addon-Tools.md b/wiki/MCP-Addon-Tools.md index 7e821e5ea..b17888cfd 100644 --- a/wiki/MCP-Addon-Tools.md +++ b/wiki/MCP-Addon-Tools.md @@ -1,6 +1,6 @@ # MCP Addon Tools and Resources -This page covers MCP tools and resources that require specific addons to be enabled on your TREK instance. For core tools (trips, places, day planning, accommodations, transport, reservations, budget, tags, maps, and notifications) see [MCP-Tools-and-Resources](MCP-Tools-and-Resources). +This page covers MCP tools and resources that require specific addons to be enabled on your TREK instance. For the rest of the surface (trips, places, day planning, accommodations, transport, reservations, tags, maps, and notifications — plus the Budget tools, which need the Budget addon but are documented there) see [MCP-Tools-and-Resources](MCP-Tools-and-Resources). --- diff --git a/wiki/MCP-Tools-and-Resources.md b/wiki/MCP-Tools-and-Resources.md index a842a3faa..145dfde27 100644 --- a/wiki/MCP-Tools-and-Resources.md +++ b/wiki/MCP-Tools-and-Resources.md @@ -76,8 +76,8 @@ Requires `trips:read` or `trips:write` scope. | `update_assignment_time` | Set start/end times for a place assignment (e.g. `"09:00"` – `"11:30"`). Pass `null` to clear. Requires `places:write`. | | `move_assignment` | Move a place assignment to a different day. Requires `places:write`. | | `set_leg_transport_mode` | Set the travel mode of a route leg for a place assignment. `direction` `"outgoing"` (default) targets the leg leaving the stop, `"incoming"` the arriving one. Pass `null` to inherit the day default. Requires `places:write`. | -| `get_assignment_participants` | Get users participating in a specific place assignment. | -| `set_assignment_participants` | Set participants for a place assignment (replaces current list). | +| `get_assignment_participants` | Get users participating in a specific place assignment. Requires `places:read` or `places:write`. | +| `set_assignment_participants` | Set participants for a place assignment (replaces current list). Requires `places:write`. | ### Day Notes diff --git a/wiki/Map-Features.md b/wiki/Map-Features.md index 0d9a7c638..fe70854d4 100644 --- a/wiki/Map-Features.md +++ b/wiki/Map-Features.md @@ -80,7 +80,7 @@ Flights, trains, cars, and cruises can be drawn as overlays between their endpoi **Bulk options**, alongside the per-booking toggle: -- **Show all / hide all** — a route icon button in the day-plan toolbar (next to Undo/Reorder) flips every routable booking on the trip on or off at once, without affecting bookings you've toggled individually. +- **Show all / hide all** — a route icon button in the day-plan toolbar (next to Undo/Reorder) flips the whole trip between showing every routable booking and showing none. It is a clean slate rather than a layer on top: whatever you had set with the per-booking **Route** icons is discarded, so pressing it twice leaves you with all routes on or all off, not back where you started. (Automated public-transit journeys have a second gate of their own — see the transit bullet.) - **Always show booking routes** (Settings → General → Travel & map) — an account-wide default that shows every booking's route automatically on any trip you haven't touched before. It sets the *default* only — a trip where you've already used the per-booking toggle or the bulk button keeps its own choice even if you change this setting afterwards. > **Tip:** Whether endpoint text labels appear on the endpoint markers is your own choice — the **Booking route labels** setting in Settings → General → Travel & map (`map_booking_labels`). It is off by default; with it off, the endpoint markers show only the transport icon. diff --git a/wiki/Map-Settings.md b/wiki/Map-Settings.md index 28f921b0b..0036b9e4e 100644 --- a/wiki/Map-Settings.md +++ b/wiki/Map-Settings.md @@ -51,7 +51,7 @@ Required scopes are: - DATASETS:READ - VISION:READ -If Mapbox GL is selected but no token is saved, the map area shows an empty state with a prompt to configure the token under Settings → Map → Mapbox GL. +If Mapbox GL is selected but no token is saved, TREK falls back to the Leaflet renderer, so the Trip Planner and Journey maps keep working. Only the preview on this settings page stays blank, with the note *Enter a Mapbox access token to preview*. **Built-in style presets:** @@ -98,7 +98,7 @@ The **3D Buildings & Terrain** and **High Quality Mode** toggles are Mapbox-only There is **no** default map center or zoom setting — it was removed in v3.4.0. Instead, every map works out its own opening camera from the places it is about to draw, so a trip in Japan opens on Japan instead of starting on the world view and then flying across the planet. -For each map, TREK takes the coordinates it is given and computes the center and zoom that frame them all: +For each trip map — Trip Planner, Collections and shared-trip links — TREK takes the coordinates it is given and computes the center and zoom that frame them all (the Journey maps frame themselves with the renderer's own `fitBounds`, capped at zoom 16 on both engines): - **Several places** — the camera fits their bounding box. The zoom is capped (16 on Leaflet, 15 on the GL renderers) so a tight cluster does not open absurdly far in, and the center is taken in projected (Mercator) space rather than as an average of the latitudes, so the northernmost place does not fall out of frame. - **A single place** (or several stacked on the same spot) — there is no extent to fit, so it opens at city level: zoom 12 on Leaflet, 11 on the GL renderers. @@ -106,7 +106,7 @@ For each map, TREK takes the coordinates it is given and computes the center and - **Side panels** — chrome overlaying the map shifts the center, so the places stay inside the part of the map you can actually see. - **No usable coordinates** — a brand-new trip with nothing placed yet has nothing to frame, so the map falls back to a world view (center `0, 0` at zoom 2). -The small map preview on this settings page is the one exception: it stays on a fixed city (Paris, zoom 16) so you can judge label density, 3D buildings and satellite texture. It is not a setting and no real map opens there. +The small map preview on this settings page is the exception: it stays on a fixed city (Paris) — zoom 16 on the two GL previews, city-level zoom 12 under Leaflet — so you can judge label density, 3D buildings and satellite texture. It is not a setting and no real map opens there. ## See also diff --git a/wiki/My-Trips-Dashboard.md b/wiki/My-Trips-Dashboard.md index c6dd7a522..fd22680b2 100644 --- a/wiki/My-Trips-Dashboard.md +++ b/wiki/My-Trips-Dashboard.md @@ -22,7 +22,7 @@ Trips without dates are treated as past. ## Spotlight Card -The first ongoing trip — or the next upcoming trip if none is ongoing — is promoted to a full-width **Spotlight card** at the top of the grid on desktop. On mobile this card appears as a hero at the top of the page. The spotlight card shows a progress bar for ongoing trips and a stats strip (days, places, travel companions). +The first ongoing trip — or the next upcoming trip if none is ongoing — is promoted to a full-width **Spotlight card** at the top of the grid on desktop, where it shows the travel companions, the trip dates, a countdown and the places as a boarding pass. On mobile this card appears as a hero at the top of the page, with a progress bar for ongoing trips and a stats strip (days, places, travel companions). If you have no trips yet, the spotlight card is not shown. @@ -44,7 +44,7 @@ See [Dashboard-Widgets](Dashboard-Widgets) for full usage details. ## Per-Trip Actions -On desktop, hover over a card (or open the row actions in list view) to reveal the action buttons — they appear on mouse-over only. On mobile, action buttons are always visible directly on the card cover. The available actions are: +On desktop the action buttons sit on the card cover at all times — hovering the card only lifts them from a slightly dimmed resting state to full opacity, and list rows carry the same buttons on their cover. On mobile, action buttons are always visible directly on the card cover. The available actions are: | Action | Permission required | |---|---| diff --git a/wiki/Offline-Mode-and-PWA.md b/wiki/Offline-Mode-and-PWA.md index 54cace816..4c4a7b060 100644 --- a/wiki/Offline-Mode-and-PWA.md +++ b/wiki/Offline-Mode-and-PWA.md @@ -41,12 +41,14 @@ TREK uses Workbox service-worker caching plus an IndexedDB database (Dexie) for **IndexedDB (Dexie) — structured trip data** -On login, after each trip-list refresh, and on WebSocket reconnect, TREK runs a background sync that writes full trip bundles into IndexedDB: +On login, when the browser comes back online, and when you lift **Force offline mode**, TREK runs a background sync that writes full trip bundles into IndexedDB — you can also start one by hand with **Re-sync now**, with **Download for offline use** for a progress-tracked run, or by re-enabling a trip's offline toggle: - Trips, days, places, packing items, to-dos, budget items, reservations, accommodations, trip members, tags, and categories. - File attachments that are neither photos nor videos (PDFs, documents, etc.) are downloaded and stored as blobs in IndexedDB. Videos are deliberately skipped — a single clip can be hundreds of megabytes and would evict the trip's real documents. - Map tiles are pre-fetched into the service-worker `map-tiles` cache for zoom levels 10–16 across each trip's bounding box, stopping at the zoom level that would push the total past 12 288 tiles (roughly 180 MB). +> **Note:** A WebSocket reconnect does *not* run this sync. It replays your queued changes and then re-reads the trip you currently have open — days, places, packing items, to-dos, budget items, reservations and files — which refreshes that one trip's cached rows. It never re-downloads the bundles for your other trips, the file blobs or the map tiles; skipping the full sync there is deliberate, so a dropped socket on an otherwise online device doesn't run into the server's rate limiter. + **Sync scope and eviction** - Only ongoing and future trips are cached (trips whose `end_date` is today or later, or has no end date). @@ -82,7 +84,7 @@ The stats panel shows cached trips, pending changes, conflicts and failed change - Offline **editing** is supported for places and packing items (with conflict detection). Other entities — budget, to-dos, reservations, days — require connectivity to edit; while forced offline those edits still go to the live server when a connection is actually present. - A change you made offline that **deletes** an item wins over a concurrent server edit of that same item ("delete wins"); only edit-vs-edit conflicts are surfaced for resolution. - The conflict token has one-second resolution, so two edits to the same field within the same second can't be told apart and fall back to last-write-wins (only relevant to sub-second races; normal offline windows are unaffected). -- New trips created while offline are queued and synced when connectivity is restored. +- Creating a trip requires connectivity. Trip creation is not queued, so a new trip cannot be started while offline. - Photo uploads require connectivity. Photo and video attachments are not pre-cached; every other file attachment is pre-cached automatically during sync. - Real-time collaboration features require an active WebSocket connection. - Mapbox GL / vector tiles are not pre-downloaded; raster (Leaflet) tiles are. With map-tile storage off, individually viewed tiles may still be cached opportunistically by the service worker. diff --git a/wiki/PDF-Export.md b/wiki/PDF-Export.md index 21f6376aa..e5242b42e 100644 --- a/wiki/PDF-Export.md +++ b/wiki/PDF-Export.md @@ -14,7 +14,7 @@ Open the Day Plan sidebar in the trip planner and click the **Export** button in ### Cover page -- Blurred cover image as background (if the trip has one), with the same image in a circular badge +- Faded cover image as background (if the trip has one), with the same image in a circular badge - Trip title and description - Date range (first day to last day) - Stat tiles: diff --git a/wiki/Packing-Lists.md b/wiki/Packing-Lists.md index 40ae34d52..9d656e813 100644 --- a/wiki/Packing-Lists.md +++ b/wiki/Packing-Lists.md @@ -50,7 +50,7 @@ Each item row contains: - A **quantity** field (always visible). - When bag tracking is enabled: a **weight** field (in grams) and a **bag picker**. -Hovering over an item reveals a **category picker** (colored dot), a **rename** button (pencil icon), and a **delete** button. Add new items using the inline "add item" row at the bottom of each category, or bring a whole list in at once with **Import** (below). +Each row also carries a **category picker** (colored dot), a **rename** button (pencil icon), and a **delete** button. Add new items using the inline "add item" row at the bottom of each category, or bring a whole list in at once with **Import** (below). On phones the row keeps the name and reduces everything else: sharing shows as an icon rather than a sentence, a weight nobody entered prints nothing, and edit/delete sit behind the **⋯** button at the end of the row while the list is in edit mode. @@ -94,7 +94,7 @@ Open an item's **Sharing** control (the share icon on the row) to move it betwee - **Personal** — *Private — only you can see it.* - **Shared with…** — pick specific trip members below the two tier options. The item then shows only on your list and on theirs. (If you're the only one on the trip, this reads *No one else on this trip yet*.) -New items inherit the view you add them in: adding an item while in **My list** makes it Personal, adding it in **Shared** makes it Common. To share an item with specific people, add it first, then open its Sharing control and choose them. +New items inherit the view you add them in: adding an item while in **My list** makes it Personal, adding it in **Shared** puts it in the shared group pool. To share an item with specific people, add it first, then open its Sharing control and choose them. Only the item's owner (the person bringing it) can change its sharing. Someone you shared an item *with* just sees it on their **My list** with a **by {name}** badge and can tick it off — they don't manage who else it's shared with. diff --git a/wiki/Packing-Templates.md b/wiki/Packing-Templates.md index c9f9f77e5..dfdbbb029 100644 --- a/wiki/Packing-Templates.md +++ b/wiki/Packing-Templates.md @@ -12,7 +12,7 @@ Applying a template copies all categories and items from the template into the c Requires the `packing_edit` permission. -The Apply Template button only appears when at least one template exists and you have edit permission. +On the desktop planner the **Apply Template** button appears as soon as at least one template exists — it is not permission-gated in the UI. A member without `packing_edit` still sees it; the server refuses the apply with `403 No permission` and the app shows a *Failed to apply template* toast. On mobile the packing tab does gate it: the action menu that holds Apply Template only opens for members who have `packing_edit`. ## Saving the current list as a template diff --git a/wiki/Password-Reset.md b/wiki/Password-Reset.md index 16055b323..5c7dd9ab8 100644 --- a/wiki/Password-Reset.md +++ b/wiki/Password-Reset.md @@ -14,7 +14,7 @@ TREK supports email-based self-service password reset. On the login page, click 2. Enter your email address and submit. 3. Open the reset link from your email (or console) — valid for **60 minutes**. 4. Enter a new password. If your account has **MFA enabled**, you must also supply a valid TOTP code or backup code before the reset completes. -5. After a successful reset you are redirected to login. **All existing sessions are invalidated** — every device is signed out immediately. +5. After a successful reset the page shows a **Password updated** confirmation ("You can now sign in with your new password.") with a **Sign In** button back to the login page — there is no automatic redirect. **All existing sessions are invalidated** — every device is signed out immediately. ### Security properties diff --git a/wiki/Places-and-Search.md b/wiki/Places-and-Search.md index a9094e8b8..2fc2612ad 100644 --- a/wiki/Places-and-Search.md +++ b/wiki/Places-and-Search.md @@ -25,7 +25,7 @@ When a key is present, the autocomplete uses the Google Places API, which can re ### Without a Google Maps API key -TREK falls back to OpenStreetMap (Nominatim) automatically — no API key needed. A notice appears above the search box explaining that OpenStreetMap is in use and that photos, ratings, and opening hours are unavailable. Results include name, address, and coordinates. +TREK falls back to OpenStreetMap (Nominatim) automatically — no API key needed. A notice appears above the search box — *Using OpenStreetMap. A Google API key adds ratings and opening hours.* Results include name, address, and coordinates. ## Place details while searching @@ -36,7 +36,7 @@ The column shows: - **Pictures** near or of the place. Click one to make it that place's thumbnail; click it again to clear the choice. The picture then appears everywhere the place does — list, map marker, itinerary, PDF export and shared trips — exactly like a [custom place image](#custom-place-image). - **A description**, when one is available. It is *not* written into the place automatically. Use **Use this text** to copy it into the description field; the button is disabled while you already have a description of your own, so nothing you wrote gets overwritten. -Every picture is credited under its thumbnail with its author, its licence and a link to the source page. Most Wikimedia Commons pictures are CC BY or CC BY-SA, which means the credit has to travel with the picture — so once you pick one, the credit stays visible under the thumbnail in the place's detail panel too. +One credit line sits under the picture grid, and it belongs to the picture in play: the tile you are hovering, or failing that the one you picked, or the first picture before you have done either. It names the author, links that name to the source page, and adds the licence with a link to its terms. The other tiles carry author and licence as a tooltip only, without the links. Google's pictures get an author line and nothing else, because Google grants no reusable licence for them. Most Wikimedia Commons pictures are CC BY or CC BY-SA, which means the credit has to travel with the picture, so once you pick one, the credit stays visible under the thumbnail in the place's detail panel too. ### Where the information comes from diff --git a/wiki/Plugin-Cookbook.md b/wiki/Plugin-Cookbook.md index db974fc99..1979db3e5 100644 --- a/wiki/Plugin-Cookbook.md +++ b/wiki/Plugin-Cookbook.md @@ -10,7 +10,9 @@ for the permission catalogue see [[Plugin Permissions|Plugin-Permissions]]. > need that user's `*_edit` permission. A read/write you're not allowed to do fails > loudly; it never silently escalates. -The complete, runnable version of these recipes is the +A runnable version of a handful of these recipes — reading a trip's places and +bookings, raising validation warnings, contributing a place-detail row, and the +entity metadata behind it — is the [`trip-doctor`](https://github.com/liketrek/TREK/tree/main/plugin-sdk/examples/trip-doctor) example plugin. @@ -137,14 +139,14 @@ carry none. ```js events: [ { on: 'file:created', async handler({ tripId, entityId }, ctx) { - await notifySlack(`New file on trip ${tripId}`) // needs http:outbound + await notifySlack(`New file on trip ${tripId}`) // needs http:outbound: } }, ] ``` Fire-and-forget on a short timeout — never blocks a core write. Trip reads are -refused (no user); use `ctx.db`, `ctx.ws.*`, or an outbound call. Your own -`plugin:*` broadcasts are never re-delivered, so handlers can't loop. +refused (no user), and so are `ctx.ws.*` broadcasts; use `ctx.db` or an outbound +call. Your own `plugin:*` broadcasts are never re-delivered, so handlers can't loop. ## Depend on another plugin — call it and hear its events @@ -324,7 +326,7 @@ Output is **data**. To store it, push it through a gated write yourself (e.g. `c ## Call a third-party API the user connected -**Needs:** `oauth:client` (and `http:outbound` for the fetch) +**Needs:** `oauth:client` (and `http:outbound:` for the fetch) The user connects the service under Settings → Plugins → Connect. The host keeps the refresh token and client secret; you only ever get a short-lived access token for the acting user. @@ -354,7 +356,7 @@ Tenant-free and cached upstream — no trip access needed. **Needs:** `jobs:run` -Two flavours: a fixed **cron job** declared in the manifest, or a **dynamic timer** you set at runtime via `ctx.scheduler`. +Two flavours: a fixed **cron job** declared on the plugin definition, or a **dynamic timer** you set at runtime via `ctx.scheduler`. ```js module.exports = { diff --git a/wiki/Plugin-Development.md b/wiki/Plugin-Development.md index 6325a2e36..45e2969d9 100644 --- a/wiki/Plugin-Development.md +++ b/wiki/Plugin-Development.md @@ -701,10 +701,12 @@ context, so media queries inside it measure the frame, and they work. ## Settings Declare settings in the manifest; TREK renders the form (you write no settings -UI). `scope: "instance"` settings are set once by the admin; `scope: "user"` -settings are per-user. `secret: true` fields are stored encrypted and delivered -decrypted through `ctx.config` (server-side only) — never to the iframe. Resolved -values arrive in `ctx.config`. +UI). `scope: "instance"` settings are set once by the admin and arrive resolved in +`ctx.config`; `scope: "user"` settings are per-user and are read one key at a time +with `await ctx.settings.get(key)` (a userless job / `onLoad` gets `undefined` — +fall back to `ctx.config` there). `secret: true` fields are stored encrypted and +delivered decrypted through whichever of the two applies (server-side only) — +never to the iframe. ### A custom settings page (`capabilities.settingsUi`) @@ -744,8 +746,8 @@ if (!token) return { status: 401, body: 'connect this plugin first' } Grant `hook:user-data` and implement either handler to honour data-subject rights. Both are **userless** — the plugin only receives the `userId` and acts on its **own** `ctx.db`: ```js +// trek-plugin.json: "permissions": ["db:own", "hook:user-data"] module.exports = definePlugin({ - permissions: ['db:own', 'hook:user-data'], async deleteUserData({ userId }, ctx) { // erasure await ctx.db.exec('DELETE FROM my_prefs WHERE user_id = ?', userId) }, @@ -989,8 +991,8 @@ place/day/reservation/accommodation/assignment/trip, `db:read:costs` for budget, file). Without that grant you get exactly the id hint, as before: ```js +// trek-plugin.json: "permissions": ["events:subscribe", "db:read:trips"] module.exports = definePlugin({ - permissions: ['events:subscribe', 'db:read:trips'], events: [ { on: 'reservation:created', async handler({ event, tripId, entityId, snapshot }, ctx) { // with db:read:trips the snapshot carries the reservation's fields @@ -1318,7 +1320,7 @@ guard optional `ctx.*` namespaces. | `db:meta` | `ctx.meta.*` — your own namespaced data on a trip/place/day/reservation/accommodation | | `db:read:users` | `ctx.users.getById` | | `events:subscribe` | receive core activity events via `events: [...]` (event name + tripId + a { entity, entityId } hint, plus a whitelisted entity **snapshot** when the plugin also holds the family's `db:read:*` grant; never a user) | -| `hook:trip-card-provider` | `hooks.tripCardProvider` — small badges on the dashboard trip cards | +| `hook:trip-card-provider` | `hooks.tripCardProvider` — small badges on the dashboard trip cards (`getCards(tripIds, ctx)` → `{ tripId, id, label, value?, icon?, tone?, url? }[]`; `id` is required — a badge without one is dropped; the host bounds every field and access-checks each tripId) | | `jobs:run` | run declared background `jobs` on their cron schedule **and** `ctx.scheduler` runtime timers → `scheduled` handler (opt-in; no user, so trip reads are refused) | | `ws:broadcast:trip` | `ctx.ws.broadcastToTrip` | | `ws:broadcast:user` | `ctx.ws.broadcastToUser` | @@ -1334,7 +1336,6 @@ guard optional `ctx.*` namespaces. | `hook:pdf-section-provider` | `hooks.pdfSectionProvider` — sections appended to the trip PDF export | | `hook:atlas-layer-provider` | `hooks.atlasLayerProvider` — per-user country tint layers on the Atlas map | | `hook:journal-entry-provider` | `hooks.journalEntryProvider` — extra rows on a journal entry card | -| `hook:trip-card-provider` | `hooks.tripCardProvider` — small badges on the dashboard trip cards (`getCards(tripIds, ctx)` → `{ tripId, label, value?, icon?, tone?, url? }[]`; host bounds every field + access-checks each tripId) | | `hook:user-data` | `deleteUserData` / `exportUserData` handlers — honour GDPR erasure (durable, retried) and data-export for a deleted/requesting user (userless; own db only) | | `hook:photo-provider` | `hooks.photoProvider` — a photo source for Memories, aggregated at `GET /api/plugin-photos/search` (see [Provider hooks](#provider-hooks)) | | `hook:calendar-source` | `hooks.calendarSource` — calendar events for the signed-in user, aggregated at `GET /api/plugin-calendar` (see [Provider hooks](#provider-hooks)) | @@ -1357,10 +1358,10 @@ guard optional `ctx.*` namespaces. | `oauth` | `{ initPath, callbackPath }` for OAuth flows. | **Page nav:** the host builds a page plugin's nav entry from the top-level `name` -and `icon` — the installed-manifest parser only consumes `capabilities.widget`, so -there is nothing else to set. `icon` must be a real lucide name: TREK resolves it at -render time and silently falls back to `Blocks`, which makes a typo invisible locally, -so `validate` rejects one. +and `icon` — there is no `capabilities.nav`, so there is nothing else to set. +`icon` must be a real lucide name: TREK resolves it at render time and silently +falls back to `Blocks`, which makes a typo invisible locally, so `validate` +rejects one. See [[Plugin Permissions|Plugin-Permissions]] for the full permission model. @@ -1374,7 +1375,7 @@ whatever else it needs. Pass a command explicitly to skip the menu (and for scri CI). `trek-plugin help ` — or `trek-plugin --help` — prints a full page for any command. -**The path is four commands**, and the other nine are steps one of them already does: +**The path is four commands**, and the other ten are steps one of them already does: ```bash trek-plugin create [name] [--type integration|page|widget|trip-page] diff --git a/wiki/Plugin-Permissions.md b/wiki/Plugin-Permissions.md index 352af5731..f79bc4ca4 100644 --- a/wiki/Plugin-Permissions.md +++ b/wiki/Plugin-Permissions.md @@ -41,7 +41,7 @@ ungranted capability is physically unreachable**, not just disallowed. See | `db:write:tags` | Create/edit/delete the acting user's own tags via `ctx.tags` | User-scoped; ownership re-checked before each write. | | `db:write:atlas` | Mark/unmark visited countries + regions and manage the bucket list via `ctx.atlas` | All rows are the **acting user's own** — no trip scoping, no cross-tenant surface. Needs the Atlas addon; unblocks e.g. AirTrail-style two-way sync. | | `db:write:vacay` | Toggle PTO days + company holidays via `ctx.vacay` | The plan is resolved **host-side from the acting user's active plan** — a plugin can never name another plan; `toggleEntry` only toggles the acting user's own day. Needs the Vacay addon. | -| `db:write:journal` | Create/edit/delete journal entries via `ctx.journal` | Gated by `journeyService.canEdit` (owner/contributor) against the acting user. Needs the Journey addon. | +| `db:write:journal` | Create/edit/delete journal entries **and create/delete whole journals** via `ctx.journal` (`createEntry`/`updateEntry`/`deleteEntry` plus `createJourney`/`deleteJourney`) | Entry writes go through the journey domain service's `canEdit`: the journal's owner, or a contributor whose role is `editor`/`owner` — a `viewer` contributor is refused. `createJourney` has no journey-level gate (there is no journey yet); it always creates a **new journal owned by the acting user**, which is how importers bootstrap the journal they then fill. `deleteJourney` is **owner-only** (`isOwner`), not `canEdit` — a contributor, even an editor, cannot delete the journal. Every call needs a bound acting user (a no-user job is refused) and the Journey addon. | | `db:write:collections` | Create/edit collections, save places, copy to a trip via `ctx.collections` | The service enforces the acting user's **per-collection role** (owner/admin/editor) itself. Needs the Collections addon. | | `db:write:files` | Attach files + manage links via `ctx.files` (`create`, `createLink`, `update`, `softDelete`) | Trip access **+** the app's `file_upload`/`file_edit`/`file_delete` rights respectively. Content arrives as base64 (10MB cap); the extension is validated against the central blocklist before touching disk; link targets must live on the same trip. | | `db:write:collab` | Post notes, polls and chat messages via `ctx.collab` | Trip access **+** `collab_edit`; needs the Collab addon. Broadcasts the same `collab:*` events as the app. | @@ -63,7 +63,7 @@ ungranted capability is physically unreachable**, not just disallowed. See | `hook:map-layer-provider` | Overlay bounded **vector layers** (polylines, polygons, metric circles) on the trip map via the `hooks.mapLayerProvider` provider hook | Implement `MapLayerProvider` in `hooks` — returns `{id, name?, features}[]` where a feature is `{type, points?/center?+radiusM?, tone?, width?, dash?, opacity?, fill?, label?}`. **Declarative only** — the host draws everything; styling is the tone palette plus clamped numerics (width 1–8, opacity 0.05–1, radius ≤2000 km) and a dash enum, so a layer can't impersonate core UI. Budgets per plugin: ≤4 layers / ≤150 features / ≤8000 vertices / ≤2000 vertices per shape; invalid or oversized shapes are dropped whole (never truncated); a failing provider is skipped. Drawn beneath TREK's own routes. Exposed at `GET /api/map-layers/:tripId`. | | `hook:route-provider` | Offer **routing profiles** the planner's route toggle can route days with, via the `hooks.routeProvider` provider hook | Implement `RouteProvider` in `hooks` and declare the profiles in `capabilities.routeProfiles` (max 3; each `{id, label, icon?}`). TREK calls `getRoute({tripId, dayId, profile, waypoints})` for exactly the profile the user picked — this is **targeted**, not a fan-out — with a 20 s timeout so the plugin can call an external solver through its declared egress. The result (`coordinates`, `distance`, `duration`, `legs`, `viaPoints?`) is validated whole (leg count must equal `waypoints−1`, ≤10000 vertices, ≤40 vias); anything malformed is discarded and the planner falls back to straight lines. Exposed at `POST /api/plugin-routes/:pluginId/:profileId` (member-gated per trip). | | `hook:day-schedule-provider` | Attach **time contributions** to the day plan via the `hooks.dayScheduleProvider` provider hook | Implement `DayScheduleProvider` in `hooks` — returns `{id, dayId, assignmentId?/reservationId?/position?, minutes?, label, tone?}[]`. Rows render under their anchor (a place/booking row, or a day's start/end) on desktop and mobile, and `minutes` is **folded into the day's route-footer total** — the one hook whose output feeds displayed timing, which is why minutes are clamped (1–1440) and `dayId` is checked against the trip's own days. ≤60 items per plugin, labels sanitized + capped (≤120); a failing provider is skipped. Exposed at `GET /api/day-schedule/:tripId`. | -| `hook:day-tint-provider` | Colour-code **whole days** in the day plan via the `hooks.dayTintProvider` provider hook | Implement `DayTintProvider` in `hooks` — returns `{dayId, tone?, color?, badgeTone?, badgeColor?, headerTone?, headerColor?, activityTone?, activityColor?, label?}[]`, one entry per day you want coloured. A day card has **three separately tintable regions** — the day-number badge (which is also the mobile day chip), the header row, and the expanded activity list — so you can mark a leg boldly on the badge and leave the dense activity list plain. `tone` / `color` are shorthands for every region you do not name; a region you never name is not tinted at all and renders exactly as it does without your plugin. Paint a region with a palette tone or with your own colour — **`#rrggbb` only**, since the value ends up inside a CSS colour the host builds; a colour beats a tone within a region, and a region's own value beats the shorthand. **You choose the hue, the host chooses the weight**: it sets the alpha per theme and per region (a large surface behind dense text gets a fainter tint than a small badge) and clamps your colour's lightness into a band that reads on both the light and the dark sidebar, so no contribution can hand a user a day card they cannot read. A tinted header hovers to a deeper mix of its own colour instead of losing it. `dayId` is checked against the trip's own days, `label` (≤60) becomes the day's tooltip, the raw array is sliced at 2000. **A day takes at most one contribution, and it is resolved whole**: within one provider's list the first entry for a day wins, and across plugins the first granted provider wins — so a day can't flicker between colours, and a losing plugin can't fill in the winner's untinted regions. Unlike `hook:day-schedule-provider` there is no fixed item cap; the bound is the trip's day count, which is what makes this usable on a six-month itinerary. A failing provider is skipped. Exposed at `GET /api/day-tints/:tripId`. | +| `hook:day-tint-provider` | Colour-code **whole days** in the day plan via the `hooks.dayTintProvider` provider hook | Implement `DayTintProvider` in `hooks` — returns `{dayId, tone?, color?, badgeTone?, badgeColor?, headerTone?, headerColor?, activityTone?, activityColor?, label?}[]`, one entry per day you want coloured. A day card has **three separately tintable regions** — the day-number badge (which is also the mobile day chip), the header row, and the expanded activity list — so you can mark a leg boldly on the badge and leave the dense activity list plain. `tone` / `color` are shorthands that paint every region you do not name; omit both and only the regions you name are tinted, and the rest render exactly as they do without your plugin. An entry that would paint nothing at all — no region named and no usable shorthand — still means "tint this day", so all three regions fall back to the `default` tone. Paint a region with a palette tone or with your own colour — **`#rrggbb` only**, since the value ends up inside a CSS colour the host builds; a colour beats a tone within a region, and a region's own value beats the shorthand. **You choose the hue, the host chooses the weight**: it sets the alpha per theme and per region (a large surface behind dense text gets a fainter tint than a small badge) and clamps your colour's lightness into a band that reads on both the light and the dark sidebar, so no contribution can hand a user a day card they cannot read. A tinted header hovers to a deeper mix of its own colour instead of losing it. `dayId` is checked against the trip's own days, `label` (≤60) becomes the day's tooltip, the raw array is sliced at 2000. **A day takes at most one contribution, and it is resolved whole**: within one provider's list the first entry for a day wins, and across plugins the first granted provider wins — so a day can't flicker between colours, and a losing plugin can't fill in the winner's untinted regions. Unlike `hook:day-schedule-provider` there is no fixed item cap; the bound is the trip's day count, which is what makes this usable on a six-month itinerary. A failing provider is skipped. Exposed at `GET /api/day-tints/:tripId`. | | `geolocation:read` | Ask the HOST for the browser's live position from the plugin's sandboxed frames (`window.trek.geolocation`) | A **bridge-level** permission — it unlocks no ctx RPC method. The sandbox itself never gains the geolocation API: the host page reads `navigator.geolocation` (the browser's own site permission prompt still applies) and posts plain `{lat, lng, accuracy, heading, speed, timestamp}` data into the frame over postMessage. `get()` resolves one fix; `watch(cb)` streams updates until unsubscribed, and the host force-stops the GPS watch when the frame closes. Nothing is sent to the server — a position only reaches the plugin's server code if its own client ships it through one of its routes (visible in that route's traffic like any other call). | | `hook:table-contributor` | Contribute host-rendered **columns/actions** into a native planner view via the `hooks.tableContributor` provider hook | Implement `TableContributor` in `hooks` — returns declarative column/action leaves keyed by `entityId` (never markup). The host normalizes + bounds every field (length caps, `http`/`https`/`mailto`-only urls, enum tone/target) and renders them in the reservations, transports, places, day, costs, packing, files and todos views; an action opens your sandboxed frame or calls one of your routes. Also exposed at `GET /api/view-contributions/:view/:tripId`. | | `hook:pdf-section-provider` | Append text-only **sections** to the trip PDF export via the `hooks.pdfSectionProvider` provider hook | Implement `PdfSectionProvider` in `hooks` — returns `{title, paragraphs?, table?}[]` of plain strings the export escapes and lays out itself; no markup ever reaches the document. The host caps the section/paragraph/header/row counts (≤5/≤20/≤8/≤50) and every string length, and clips table rows to the header width; a failing provider is skipped. Exposed at `GET /api/pdf-sections/:tripId`. | @@ -86,14 +86,16 @@ Two independent guards restrict a plugin's network, and **both are built from th - the plugin iframe's **CSP `connect-src`** (the client can only fetch the same hosts). -`egress[]` is a **separate declaration** the manifest validator only checks for -*presence*, not contents. The rule it enforces is narrow: +`egress[]` is a **separate declaration**: the manifest validator checks its shape, +but never cross-checks it against the `http:outbound:` permissions you +granted. The rules it enforces are narrow: - Only the permissions above are accepted; an unknown string fails validation. - If **any** `http:outbound` permission (bare or per-host) is declared, `egress[]` must be **non-empty** — *unless* the manifest sets `operatorEgress: true`, whose hosts arrive from the admin after install (see below). -- `egress[]` may not contain a bare `*`. +- Every entry must be a bare host or a `*.suffix` wildcard: a bare `*`, a whole-TLD + `*.com` and anything carrying a scheme are rejected. ### `operatorEgress` — hosts only the operator knows @@ -158,10 +160,10 @@ flow is in [[Publishing a Plugin|Plugin-Publishing]]. |---|---| | `trek-plugin create [name]` | Scaffold a plugin. With no name it runs an interactive wizard (id, type, author, permissions); with a name it takes `--type`/`--author`/`--permissions` flags. | | `trek-plugin dev [dir]` | Run the plugin locally with a real request loop + hot reload — no full TREK. The injected `ctx` enforces your granted permissions, `db:own` is a real SQLite file, routes serve under `/api/`, and page/widget UI at `/ui`. | -| `trek-plugin validate [dir]` | Manifest + layout checks: parses the manifest with the same rules as install, requires a `README.md` (warns if it has no screenshot or still holds template placeholders) and a built `server/index.js`, and warns if the directory name ≠ the plugin id. This is a **subset** of registry CI — CI additionally verifies the release tag/commit, the artifact's sha256, and the README over the network. A local pass predicts a CI pass. | +| `trek-plugin validate [dir]` | Manifest + layout checks, and the gate for publishing: parses the manifest with the same rules as install, then runs every registry gate that can be answered from the working tree — exiting non-zero on any of them. It **fails** if `README.md` is missing, is missing one of the four required sections, has too little prose, still holds template placeholders, does not explain every permission the manifest declares, or has no usable screenshot (no image at all, or every local image path is missing on disk — a remote `https://` image passes here and is verified later by `preflight`); it also fails if `server/index.js` was never built. Failures that only the registry cares about do **not** block `trek-plugin pack`, so the dev loop keeps working while the docs are still a stub — but they do stop `publish`, which runs these same checks first and refuses before anything is packed, tagged or released. A handful of checks are *warnings* that never fail the command: directory name ≠ plugin id, an unbounded `trek` range, an unknown required addon, emoji in manifest text, an undeclared egress host, and a page/widget not using the design kit. This is a **subset** of registry CI — CI additionally verifies the release tag/commit, the artifact's sha256, and the README over the network. A local pass predicts a CI pass. | | `trek-plugin preflight --repo --tag ` | Runs the **full** registry CI checks locally over the network (tag→commit, manifest parity, artifact sha256/size, native scan, README quality gate) against your pushed release — so you catch a CI failure before opening the PR. | | `trek-plugin submit --repo --tag ` | Opens the registry PR for you: forks TREK-Plugins, fast-forwards the fork, branches off the registry's current main, writes/merges `registry/plugins/.json`, pushes, and creates the PR. Requires `gh`. | -| `trek-plugin publish --repo --tag ` | **The one-command release**: pack → tag + GitHub release → preflight → open the registry PR. A failure after the release is cut rolls back the tag and release that run created, so the same tag can be re-used (`--keep-release` opts out). Add `--sign` to sign it. Requires `git` + `gh`. | +| `trek-plugin publish --repo --tag ` | **The one-command release**: local gates → pack → tag + GitHub release → preflight → open the registry PR. Nothing is packed, tagged or released until the local gates pass (`--no-checks` skips them). A failure after the release is cut rolls back the tag and release that run created, so the same tag can be re-used (`--keep-release` opts out). Add `--sign` to sign it. Requires `git` + `gh`. | | `trek-plugin unrelease --repo ` | Deletes a stranded GitHub release + remote tag + local tag. Refuses a version that is published in the registry (its artifact is immutable); `--yes` consents when the registry index can't be checked. | | `trek-plugin keygen` / `sign` | `keygen` creates an Ed25519 signing key; `sign` (or `--sign` on `entry`/`release`/`submit`) signs the artifact and fills `authorPublicKey` + `signature` so TREK pins your identity (TOFU). | | `trek-plugin pack [dir] [--out plugin.zip] [--json]` | Validates, then builds `plugin.zip` in the installer's exact layout (`trek-plugin.json`, `README.md`, `LICENSE`, `package.json` at the root; `server/` and `client/` recursed) and prints its **sha256 + byte size**. Skips `node_modules`, `.git`, `.ts` and `.map` files, and **refuses native binaries** (`.node`, `binding.gyp`, `prebuilds/`) and over-size archives, same as the installer. **`docs/` is intentionally NOT shipped** — the store fetches your screenshot from `docs/screenshot.png` in the repo. | diff --git a/wiki/Plugins.md b/wiki/Plugins.md index 68bf553a2..8ac2fd204 100644 --- a/wiki/Plugins.md +++ b/wiki/Plugins.md @@ -141,7 +141,7 @@ blue pulse = starting, red = error, amber = disabled/incompatible, faint = inact the name and version, a **Reviewed** shield if applicable, a **Sideloaded** tag for manually-uploaded plugins (see [Installing](#installing-a-plugin)), and **capability chips** derived from its declared permissions — "Reads your trips", -"Reads costs" / "Writes costs", "Dashboard widget", "Real-time updates", +"Reads your costs" / "Adds costs", "Dashboard widget", "Real-time updates", "Provides photos", outbound hosts, and so on — so a plugin's real reach is legible without opening anything. @@ -237,9 +237,11 @@ install. But a plugin that talks to a **self-hosted service** — a Gotify, an n Uptime Kuma — cannot know *your* hostname when it is published. Such a plugin declares `operatorEgress`, and you supply the hosts yourself. -The plugin card shows a **"+ hosts"** chip when it works this way. Open **⋯ → Allowed -hosts** and add the hostname (e.g. `gotify.mydomain.com`). TREK restarts the plugin so it -picks up the new list. +Before install, the detail modal marks such a plugin with a **"+ hosts you add"** pill +under *Connects to*. On the installed row it shows an **Add allowed host** chip +(**{n} allowed host(s)** once you have added some). Open **⋯ → Allowed hosts** and add +the hostname (e.g. `gotify.mydomain.com`). TREK restarts the plugin so it picks up the +new list. What this does *not* let anyone do: @@ -265,7 +267,8 @@ plugin took while acting for you — across all plugins, newest first: each trip cost it read, each place it wrote, each outbound call TREK made on its behalf. This is the user-facing half of TREK's tamper-evident (hash-chained) plugin -audit: admins see the per-plugin view in **Admin → Plugins**, while this view is +audit: the same chain is readable per plugin over the admin API +(`GET /api/admin/plugins/:id/audit`), while this view is **never admin-gated** — anyone can review what was done with their own data. It's what keeps a plugin's deliberately broad read grants accountable to the person whose data is read. See [[Plugin Permissions|Plugin-Permissions]] for what each diff --git a/wiki/Public-Share-Links.md b/wiki/Public-Share-Links.md index 86042f89a..90fc3c09d 100644 --- a/wiki/Public-Share-Links.md +++ b/wiki/Public-Share-Links.md @@ -34,7 +34,7 @@ When creating or updating a share link you choose what the recipient can see. Th | Toggle | Default | What it shows | |--------|---------|---------------| -| **Map & Plan** (`share_map`) | Always on | The Plan tab with the interactive map and day-by-day itinerary. This toggle is locked on and cannot be disabled from the UI. | +| **Map & Plan** (`share_map`) | Always on | The Plan tab with the interactive map and day-by-day itinerary. This toggle is locked on and cannot be disabled from the UI; a link whose flag was turned off outside the UI has no Plan tab, and the server withholds its days, places and notes entirely. | | **Bookings** (`share_bookings`) | **On** | The Bookings tab with reservations and transport. Also controls whether transport items appear inline in the day plan. | | **Packing** (`share_packing`) | Off | The packing list tab, grouped by category | | **Costs** (`share_budget`) | Off | The Costs tab with a total summary and line items grouped by category | @@ -50,7 +50,7 @@ A public viewer has no account, so there is no "their" display currency to use. The shared trip page renders a branded read-only interface with a dark hero header showing the trip title, description, and date range. A tab bar at the top provides access to the sections you enabled. The viewer can switch the display language using a language picker in the top-right corner. -The Plan tab is always available. A **day picker** sits directly above the map — an **All** pill plus one pill per day (**Day 1**, **Day 2**, …) — and drives the same selection as the day cards below it, so the map and the expanded day never disagree. Pick a day and its stops are numbered on the map in visiting order (a place the day returns to shows both positions on one marker, e.g. `1 · 3`) and joined by a dashed connector. That connector is a straight line showing sequence, not a driving route: TREK will not send a shared itinerary to a third-party routing service on an anonymous visitor's behalf. On **All** the map shows every geocoded place as an unnumbered pin with no connector. Below the map sits a collapsible day-by-day itinerary (with places, notes, and transport inline when Bookings is enabled) and accommodation badges per day. +The Plan tab appears whenever **Map & Plan** is on — which is every link created through the share UI, since that toggle is locked on there. A link whose `share_map` flag was turned off outside the UI (through the REST API or the `create_share_link` MCP tool, both of which take it as a plain boolean) has no Plan tab at all: the server withholds the days, places, assignments and notes entirely, and the viewer opens on the first section the owner did share. A **day picker** sits directly above the map — an **All** pill plus one pill per day (**Day 1**, **Day 2**, …) — and drives the same selection as the day cards below it, so the map and the expanded day never disagree. Pick a day and its stops are numbered on the map in visiting order (a place the day returns to shows both positions on one marker, e.g. `1 · 3`) and joined by a dashed connector. That connector is a straight line showing sequence, not a driving route: TREK will not send a shared itinerary to a third-party routing service on an anonymous visitor's behalf. On **All** the map shows every geocoded place as an unnumbered pin with no connector. Below the map sits a collapsible day-by-day itinerary (with places, notes, and transport inline when Bookings is enabled) and accommodation badges per day. The Chat tab (when enabled via `share_collab`) shows chat messages grouped by date with sender avatars. Viewers cannot send messages. diff --git a/wiki/Reservations-and-Bookings.md b/wiki/Reservations-and-Bookings.md index b746398a8..0cdde4637 100644 --- a/wiki/Reservations-and-Bookings.md +++ b/wiki/Reservations-and-Bookings.md @@ -60,7 +60,7 @@ Each card displays: - Flights: airline name, flight number - Trains: train number, platform, seat - Hotels: check-in window, check-out time (see [Accommodations](Accommodations)) -- **Location / address** — for non-hotel, non-transport types +- **Location / address** — whenever the booking has one; for a hotel this is the address from its hotel block - **Linked accommodation** — hotel name, if this reservation is linked to an accommodation record - **Day-plan assignment** — the day and place this reservation is linked to - **Link** — the booking URL, opened in a new tab. A link with a scheme TREK refuses to open is shown as plain text instead @@ -143,7 +143,7 @@ With the **AirTrail** integration addon enabled and your instance connected unde When selected flights form a connection — each leg departs from the airport the previous one landed at, onward within 24 hours — the picker groups them and offers to **import them as one flight with a layover**. The offer is on by default; untick it to keep separate bookings. A joined booking keeps each leg's own airline, flight number, times and seat, the connection airport becomes a layover **stop** on the route, and each leg files into its own day in the planner. Since a stop is not a destination, the layover country no longer shows up as visited in Atlas. -AirTrail itself has no multi-leg flights, so a joined booking is imported **without live sync** (it shows the *Not synced* badge). Its source flights stay recognised — the picker will not offer them for import again. The same applies when you add a stop to a synced single flight by hand: the booking detaches from AirTrail instead of syncing a shape AirTrail cannot represent. +AirTrail itself has no multi-leg flights, so a joined booking is imported **without live sync**. It keeps the blue **AirTrail** badge — hover it and the tooltip says the layover has no single AirTrail flight to sync back to. The grey *Not synced* badge means something else: that flight was removed in AirTrail. Its source flights stay recognised — the picker will not offer them for import again. The same applies when you add a stop to a synced single flight by hand: the booking detaches from AirTrail instead of syncing a shape AirTrail cannot represent. ## Editing and deleting diff --git a/wiki/Reverse-Proxy.md b/wiki/Reverse-Proxy.md index 11fc301ec..7006683b2 100644 --- a/wiki/Reverse-Proxy.md +++ b/wiki/Reverse-Proxy.md @@ -14,7 +14,7 @@ Putting TREK behind a TLS-terminating reverse proxy is strongly recommended for Whatever proxy you use, it must satisfy three constraints: 1. **WebSocket upgrades on `/ws`** — TREK uses WebSockets for real-time sync. Set `proxy_read_timeout 86400` (Nginx) or rely on Caddy's automatic upgrade handling. -2. **Body size ≥ 500 MB** — backup restore ZIPs can include the full uploads directory. Set `client_max_body_size 500m` (Nginx) or `request_body_max_size 500mb` (Caddy) if you restore large backups. +2. **Body size ≥ 500 MB** — backup restore ZIPs can include the full uploads directory. Set `client_max_body_size 500m` (Nginx); Caddy imposes no request-body limit of its own, so it already passes them — just keep any `request_body { max_size … }` block you have at `500mb` or more. 3. **Pass the `Mcp-Session-Id` header through on `/mcp`** — if you use MCP. See below. ## Nginx @@ -43,8 +43,11 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 86400; - # File uploads are capped at 50 MB; backup restore ZIPs can include the full - # uploads directory and may exceed that — raise this value if restores fail. + # Documents are capped at 50 MB and photos/covers at 20 MB, but video + # uploads go up to 500 MB — both journey gallery clips and videos in a + # trip's file manager. Backup restore ZIPs can include the full uploads + # directory and may exceed even that (see BACKUP_UPLOAD_LIMIT_MB, default + # 500) — raise this value if uploads or restores fail. client_max_body_size 500m; } @@ -75,11 +78,13 @@ trek.yourdomain.com { } ``` -For large backup restores, add: +Caddy applies no request-body limit by default, so large backup restores already pass through the config above. Only add `request_body` if you want to cap uploads deliberately — and then keep the cap at 500 MB or more. It is a block, not a one-line directive: ``` trek.yourdomain.com { - request_body max_size 500mb + request_body { + max_size 500mb + } reverse_proxy localhost:3000 } ``` diff --git a/wiki/Security-Hardening.md b/wiki/Security-Hardening.md index 09d1c538b..fea986ee5 100644 --- a/wiki/Security-Hardening.md +++ b/wiki/Security-Hardening.md @@ -14,7 +14,7 @@ A production TREK deployment checklist. All items reference actual TREK configur - [ ] Set `TRUST_PROXY=1` so client IPs are captured correctly in the audit log. In `NODE_ENV=production` this defaults to `1` automatically, but set it explicitly if you use a non-standard proxy hop count. - [ ] Set `FORCE_HTTPS=true` to 301-redirect HTTP to HTTPS and add `upgrade-insecure-requests` to the CSP. Your proxy must send `X-Forwarded-Proto: https` (or terminate TLS on the same connection), otherwise the redirect fires on every request and loops. - [ ] Know that HSTS (`max-age=31536000`) does not depend on `FORCE_HTTPS`: it is sent whenever `FORCE_HTTPS=true` **or** `NODE_ENV=production`, which the Docker image sets by default — so an instance behind Traefik, Caddy or a Cloudflare Tunnel advertises HSTS without setting `FORCE_HTTPS` at all. Set `HSTS_INCLUDE_SUBDOMAINS=true` to add `includeSubDomains`; it is off by default so an install on an apex domain does not force HTTPS onto sibling subdomains you may still serve over plain HTTP. -- [ ] Keep `ALLOW_INTERNAL_NETWORK=false` unless Immich or Synology is on your LAN. See [Internal-Network-Access](Internal-Network-Access). Note: loopback (`127.x`, `::1`) and link-local (`169.254.x`) addresses are always blocked regardless of this setting. +- [ ] Keep `ALLOW_INTERNAL_NETWORK=false` unless a strict-guard integration — Immich, Synology Photos, AirTrail, a notification webhook or ntfy — is on your LAN. See [Internal-Network-Access](Internal-Network-Access). Note: loopback (`127.x`, `::1`) and link-local (`169.254.x`) addresses are always blocked regardless of this setting. ## Authentication @@ -71,7 +71,7 @@ Installed plugins run **untrusted third-party code**. TREK contains a plugin in - [ ] Leave the plugin system's defaults in place. It is **on by default** but installed plugins still have to be **activated one by one**, so no third-party code runs until an admin turns a specific plugin on. Set `TREK_PLUGINS_ENABLED=false` (accepts `false`/`0`/`off`/`no`) to switch the whole system off — installed plugins stay on disk, deactivated, and the runtime is idle. - [ ] Keep the **OS permission jail** enabled (the default). In production each plugin runs in an isolated child process launched with Node's `--permission` model: filesystem **writes**, `child_process`, worker threads and native addons are denied outright, and reads are scoped to just the plugin's own code — so a plugin cannot read `trek.db` or the secret files, or shell out. The child's environment is scrubbed (no `JWT_SECRET`, no DB credentials). Setting `TREK_PLUGIN_PERMISSIONS=off` disables this jail (isolation then falls back to crash-only) and logs a loud warning — only ever do this on a machine you fully trust. -- [ ] Rely on the **private-egress block** (SSRF backstop). Even a plugin that declared an outbound host cannot reach a destination that resolves to a loopback, private, link-local, ULA, carrier-grade-NAT, cloud-metadata (`169.254.169.254`), multicast or reserved address — the guard re-checks the resolved IP, so a plugin can't pivot to internal services or DNS-rebind to them. This is independent of `ALLOW_INTERNAL_NETWORK` (which only governs core Immich/Synology features). The one escape hatch is `TREK_PLUGIN_ALLOW_PRIVATE_EGRESS=on`, for a plugin that has to reach a service on your LAN (a Gotify, an ntfy, an Ollama). It is instance-wide rather than per plugin, and it lifts the block entirely rather than narrowing it: a declared host may then resolve to any address, the `169.254.169.254` metadata IP included, and unix-socket or named-pipe connects (`docker.sock`, a local database socket) are permitted without even being declared, since the OS jail does not gate socket connects. Leave it unset unless a specific plugin genuinely needs a LAN target. +- [ ] Rely on the **private-egress block** (SSRF backstop). Even a plugin that declared an outbound host cannot reach a destination that resolves to a loopback, private, link-local, ULA, carrier-grade-NAT, cloud-metadata (`169.254.169.254`), multicast or reserved address — the guard re-checks the resolved IP, so a plugin can't pivot to internal services or DNS-rebind to them. This is independent of `ALLOW_INTERNAL_NETWORK` (which governs the core integrations reached through the strict guard — Immich, Synology Photos, AirTrail, notification webhooks and ntfy — not plugin egress). The one escape hatch is `TREK_PLUGIN_ALLOW_PRIVATE_EGRESS=on`, for a plugin that has to reach a service on your LAN (a Gotify, an ntfy, an Ollama). It is instance-wide rather than per plugin, and it lifts the block entirely rather than narrowing it: a declared host may then resolve to any address, the `169.254.169.254` metadata IP included, and unix-socket or named-pipe connects (`docker.sock`, a local database socket) are permitted without even being declared, since the OS jail does not gate socket connects. Leave it unset unless a specific plugin genuinely needs a LAN target. - [ ] The supervisor caps each plugin's **resident memory** (default 300 MB, `TREK_PLUGIN_MAX_RSS_MB`) — measured host-side from the OS, never the plugin's self-report — and kills a plugin that blows the ceiling or stops sending heartbeats; repeat offenders auto-disable. Every `ctx.*` capability call is also **rate-limited** at the dispatch boundary (a token bucket: ~60-call burst, 20 calls/sec sustained, 16 concurrent; `TREK_PLUGIN_RPC_BURST` / `TREK_PLUGIN_RPC_PER_SEC` / `TREK_PLUGIN_RPC_INFLIGHT`), so one plugin in a tight loop gets throttled instead of freezing the instance. - [ ] Review the **capability audit** if you grant plugins broad data access. Every host-mediated core-data read and broadcast a plugin makes is recorded at the RPC boundary against the real acting user (not a value the plugin supplies) in a per-plugin, hash-chained, tamper-evident log. Admins see it per plugin; each user can see "what have plugins done in my name?". Retention is capped per plugin (default 20 000 rows, `TREK_PLUGIN_AUDIT_MAX_ROWS`). diff --git a/wiki/Tags-and-Categories.md b/wiki/Tags-and-Categories.md index cdb8274db..6e357edaf 100644 --- a/wiki/Tags-and-Categories.md +++ b/wiki/Tags-and-Categories.md @@ -3,7 +3,7 @@ TREK has two independent labelling systems for places: - **Global Place Categories** — admin-managed, shared across every user on the instance (e.g. `Restaurant`, `Museum`). -- **Personal Tags** — user-scoped, private labels (e.g. `hidden gem`, `kid-friendly`). +- **Personal Tags** — user-scoped labels (e.g. `hidden gem`, `kid-friendly`). @@ -28,18 +28,24 @@ Categories appear in: ## Personal Tags -Tags are private labels owned by each user. They attach to individual places via a many-to-many relationship (`place_tags` table), so the same tag can be applied to as many places as you like, and a single place can carry multiple tags. +Tags are personal labels owned by each user. They attach to individual places via a many-to-many relationship (`place_tags` table), so the same tag can be applied to as many places as you like, and a single place can carry multiple tags. **Fields per tag:** - **Name** — free-form text. - **Color** — hex value displayed alongside the tag name. Default: `#10b981` (emerald). -Tags are scoped to their creator — other trip members do not see your tags, and different users can create tags with identical names without conflict. Deleting a tag automatically removes it from every place it was attached to. +Tags are owned by their creator, but only the *list* is private. `GET /api/tags` and the `list_tags` MCP tool return only your own tags, and tag names carry no uniqueness constraint, so different users — or even the same user — can create tags with identical names without conflict. + +A tag you attach to a place is **not** private. It becomes part of that place: the place read-back joins `place_tags` → `tags` with no owner filter, so every trip member gets it from `GET /api/trips/:id/places`, the day and assignment endpoints, and the MCP `list_places` tool and `trek://trips/{tripId}/places` resource — gated by trip access alone. The `?tag=` filter on the places list is likewise not user-scoped, so a member can filter by a co-traveller's tag ID. This is deliberate: a tag is kept as long as its owner is on the trip roster, so a co-traveller's tag is not silently stripped when another member re-saves the place. A tag owned by someone outside the trip is dropped instead. + +If a trip has a public share link with the map/itinerary section enabled, the tags of places **scheduled on a day** also reach anonymous viewers — trimmed to the tag's ID, name and color, so the owner's user ID never leaves the server. Unscheduled places in the shared place pool carry no tags. + +In practice these tags surface through the API and MCP rather than in the app — no view renders a place's tags yet. Deleting a tag automatically removes it from every place it was attached to. ### Where to manage them -At the moment tags are exposed primarily through the MCP API — AI assistants connected to your instance can list, create, update, and delete tags (`list_tags`, `create_tag`, `update_tag`, `delete_tag`) and attach them to places through the place endpoints. A dedicated web UI for tag management is not yet available; the filter `tag` parameter on the places API / MCP resource does support filtering places by a tag ID once one exists. +At the moment tags are exposed primarily through the MCP API — AI assistants connected to your instance can list, create, update, and delete tags (`list_tags`, `create_tag`, `update_tag`, `delete_tag`) and attach them to places through the place endpoints. A dedicated web UI for tag management is not yet available; the filter `tag` parameter on the places API and on the MCP `list_places` tool does support filtering places by a tag ID once one exists (the `trek://trips/{tripId}/places` MCP resource only filters by `assignment`). > **AI / MCP:** See [MCP-Tools-and-Resources](MCP-Tools-and-Resources) for the full tag tool list. diff --git a/wiki/Todos-and-Tasks.md b/wiki/Todos-and-Tasks.md index e729a2279..a720b137c 100644 --- a/wiki/Todos-and-Tasks.md +++ b/wiki/Todos-and-Tasks.md @@ -63,7 +63,7 @@ Toggle the **Priority** button in the sidebar to sort the current task list from ## Adding tasks -Click the **+ Add task** button in the top-right corner of the Lists panel header (visible when the **Todo** sub-tab is active). A new-task form opens as a modal where you can set all fields before saving. On mobile it slides up from the bottom of the screen. +Click the **+ Add new task** button in the top-right corner of the Lists panel header (visible when the **To-Do** sub-tab is active). A new-task form opens as a modal where you can set all fields before saving. On mobile it slides up from the bottom of the screen. ## Permissions diff --git a/wiki/Transport-Flights-Trains-Cars.md b/wiki/Transport-Flights-Trains-Cars.md index be21fb196..18fde44b5 100644 --- a/wiki/Transport-Flights-Trains-Cars.md +++ b/wiki/Transport-Flights-Trains-Cars.md @@ -6,11 +6,11 @@ Log flights, trains, car rentals, and cruises with departure and arrival endpoin ## Where to create -Open the **Transports** tab in the trip planner and click **Add**, or open the planner from a day view and use the transport shortcut. Transport records appear in the [Reservations](Reservations-and-Bookings) panel alongside other bookings. +Open the **Transports** tab in the trip planner and click **Add**, or open the planner from a day view and use the transport shortcut. Transport records stay on the **Transports** tab — the [Bookings](Reservations-and-Bookings) tab lists only the non-transport reservations (accommodation, restaurant, event, tour, parking, other), so the same record never shows up on both. Transports do appear next to your other bookings in two other places: inline in the day plan, and on a [public share link](Public-Share-Links), whose single **Bookings** tab lists reservations and transport together. ## Public transit search -The **Add transport** dialog has two modes: **Manual transport** (the classic form) and **Automated transport** — a public-transit route search powered by [Transitous](https://transitous.org/), free open data with no API key or paid provider. The **transit button** (tram icon) on each day header opens the dialog straight in the Automated mode. (The rename pencil this button replaced moved next to the day name in the day detail panel.) +The **Add transport** dialog has two modes: **Manual** (the classic form) and **Automated** — a public-transit route search powered by [Transitous](https://transitous.org/), free open data with no API key or paid provider. The **transit button** (tram icon) on each day header opens the dialog straight in the Automated mode. (The rename pencil this button replaced moved next to the day name in the day detail panel.) You can also start the search from a single leg: click the travel-time connector between two stops in the day plan and pick **Public transit** from the menu. The search opens with that leg's start and end already filled in and the departure time taken from the stop you are leaving. @@ -19,7 +19,7 @@ The mode switch only appears when the trip has a **start date and an end date** - Pick **from** and **to** (stop/station search; the day's own places and hotels appear as quick picks), a **depart/arrive** time, and filter by mode: train, subway, tram, bus, ferry, cable car. - Rank the results by **best route**, **fewer transfers**, or **less walking**. - Each result shows local departure/arrival times, duration, transfers, walking time and the line badges in their official colors; expand it for the stop-by-stop breakdown. -- **Add to day** saves the chosen connection as a first-class **transit** entry. It slots into the day timeline at its departure time and shows its line badges, transfers and walking time right in the plan. Clicking it opens the **journey view**: the full stop-by-stop itinerary together with the editable booking fields (title, status, confirmation code, notes), a **Change route** action that re-runs the search and replaces the itinerary, and delete. In the Transports tab these journeys appear in their own **Automated public transit** section. +- **Add to day** saves the chosen connection as a first-class **transit** entry. It slots into the day timeline at its departure time and shows its line badges, transfers and walking time right in the plan. Clicking it opens the **journey view**: the full stop-by-stop itinerary together with the editable title and notes, a **Change route** action that re-runs the search and replaces the itinerary, and delete. In the Transports tab these journeys appear in their own **Automated public transit** section. Self-hosters can point the `TRANSIT_API_URL` environment variable at their own MOTIS instance. @@ -54,16 +54,19 @@ Once you select an airport, the **timezone** for that airport appears next to th ### Trains, cars, and cruises -Departure and arrival fields use the **generic location picker** — search by place name or enter a free-text location. Results come from the maps search service. +Departure and arrival fields use the **generic location picker** — type at least three characters and pick one of the search results; a name that is only typed, never picked, is not saved. Results come from the maps search service. For the **Car** type the date fields are relabelled to match a rental: the departure side reads **Pickup** and **Pickup time**, the arrival side **Return** and **Return time**. There is no separate car-rental type — a rental and your own car are both logged as Car. ## Flight-specific fields -When the type is set to Flight, two additional fields appear: +When the type is set to Flight, three additional fields appear on every leg: - **Airline** — carrier name (e.g. Lufthansa) - **Flight number** — (e.g. LH 123) +- **Seat** — seat number (e.g. 12A) + +A flight with more than two airports gets a fourth per-leg field, its own **booking / confirmation code**, so each segment of a connecting itinerary can carry its own reference. ## Train-specific fields (multi-leg route) diff --git a/wiki/Trip-Members-and-Sharing.md b/wiki/Trip-Members-and-Sharing.md index 87e563d03..971f67d3a 100644 --- a/wiki/Trip-Members-and-Sharing.md +++ b/wiki/Trip-Members-and-Sharing.md @@ -4,8 +4,8 @@ ## Opening the Members Panel -- From the **dashboard**: click the share/members icon on a trip card. - From the **trip planner**: click the Share button in the top navigation bar. +- On **mobile**: open the trip, then **More** → **Share Trip**. When you have the `share_manage` permission the modal opens to a two-column layout on wider screens (members on the left, share link on the right). Without that permission only the members column is shown. On narrow screens the columns always stack. @@ -111,7 +111,7 @@ Changes to toggles take effect immediately for an existing link. ### Deleting a Link -Click **Delete link** (red button below the URL) to revoke access. The token is invalidated and existing viewers are redirected. +Click **Delete link** (red button below the URL) to revoke access. The token is invalidated; anyone who opens the old URL now sees *Link expired or invalid*. ## Trip Invite Link diff --git a/wiki/Trip-Planner-Overview.md b/wiki/Trip-Planner-Overview.md index aeff95034..957116dc0 100644 --- a/wiki/Trip-Planner-Overview.md +++ b/wiki/Trip-Planner-Overview.md @@ -51,7 +51,7 @@ On screens narrower than 768 px, TREK does not squeeze the three-pane layout — ## Undo -The planner tracks your recent actions — adding places, assigning them to days, reordering, and removing assignments — in a short undo ring. The **Undo** button appears in the Day Plan Sidebar toolbar (at the top of the sidebar) whenever an undoable action is available. It shows the name of the last action as a tooltip on hover and reverses it when clicked. +The planner tracks your recent actions — adding places, assigning them to days, reordering, and removing assignments — in a short undo ring. The **Undo** button sits in the Day Plan Sidebar toolbar (at the top of the sidebar); it is greyed out until an undoable action is available. It shows the name of the last action as a tooltip on hover and reverses it when clicked. ## Splash Screen diff --git a/wiki/Troubleshooting.md b/wiki/Troubleshooting.md index 5293a3461..555d8fdb2 100644 --- a/wiki/Troubleshooting.md +++ b/wiki/Troubleshooting.md @@ -107,7 +107,11 @@ If you are accessing TREK directly on `http://:3000` with no proxy, remove **Fix:** If you still have access to your account, use one of the 10 backup codes generated during MFA setup to complete login. After signing in, go to **Settings > Account** to disable or reconfigure MFA. -If you no longer have access to backup codes and cannot log in, an admin must disable MFA for your account directly in the database, or use the `reset-admin.js` script to regain access to an admin account. There is no per-user MFA reset in the Admin Panel UI — the Admin Panel only controls the global "require MFA for all users" policy. See [Admin: Users and Invites](Admin-Users-and-Invites). +If you no longer have access to backup codes and cannot log in, an admin can clear your MFA for you: `DELETE /api/admin/users//mfa` (admin session required). It clears `mfa_enabled`, `mfa_secret` and `mfa_backup_codes` — the same three columns a self-service disable clears — and is written to the audit log as `admin.user_mfa_reset`. + +The endpoint refuses to reset the caller's own account (*"Use Settings to change your own two-factor setup"*), so an admin cannot unlock themselves with it. If the locked-out account is the only admin, run `reset-admin.js` with a **new** `RESET_ADMIN_EMAIL` to create a second admin account — for an email that already exists the script only resets the password and role and leaves MFA untouched, so you would still hit the TOTP prompt — then sign in as that account and clear the first one's MFA. Hand-editing the database is the last resort, not the only route. + +There is no button for it: the Admin Panel UI has no per-user MFA reset (the user modal offers only **Reset passkeys**) — it only controls the global "require MFA for all users" policy. See [Admin: Users and Invites](Admin-Users-and-Invites). --- @@ -145,14 +149,22 @@ Keep the proxy's `client_max_body_size` at or above `BACKUP_UPLOAD_LIMIT_MB`. No ## "Cannot find module" on startup -**Likely cause:** A Docker volume mount is missing or the `/app/data` and `/app/uploads` directories are not writable by the container process. TREK automatically creates all required subdirectories on startup (`data/logs`, `data/backups`, `data/tmp`, `uploads/files`, `uploads/covers`, `uploads/avatars`, `uploads/photos`) — if this fails because the volume is read-only or owned by the wrong user, startup will abort. +**Likely cause:** A volume is mounted at `/app`, which hides the application code (`node_modules` and `dist`) shipped inside the image. Mount only the data and uploads directories — `-v ./data:/app/data -v ./uploads:/app/uploads` — never `/app` itself. Current images detect this before Node starts and print `FATAL: TREK application files are missing from the image.` instead of the bare module error. -**Fix:** Check your Docker volume configuration. Both `./data:/app/data` and `./uploads:/app/uploads` must be mounted and writable. Run `docker inspect --format '{{json .Mounts}}'` to verify the mounts are present and point to valid host paths. If the host directories are owned by root, the container's `chown` step (which runs as root before dropping to `node`) should correct permissions automatically — but if your host filesystem is read-only or permissions are locked down, grant write access manually: +**Fix:** List your mounts and remove any that target `/app`: ```bash -sudo chown -R 1000:1000 ./data ./uploads +docker inspect --format '{{json .Mounts}}' ``` +Keep only `./data:/app/data` and `./uploads:/app/uploads`, then recreate the container. Your data in those two directories is preserved when you switch. + +> **Note:** Unwritable `data`/`uploads` directories are a *different* failure — they abort with a permission error (`EACCES`), not with `Cannot find module`. TREK creates the subdirectories it needs on startup (`data/logs`, `data/backups`, `data/tmp`, `uploads/files`, `uploads/covers`, `uploads/avatars`, `uploads/photos`, `uploads/journey`, `uploads/places`). The container's `chown` step (which runs as root before dropping to `node`) normally corrects ownership, but if your host filesystem is read-only or permissions are locked down, grant write access manually: +> +> ```bash +> sudo chown -R 1000:1000 ./data ./uploads +> ``` + --- ## Container won't start: "exec /usr/bin/dumb-init: operation not permitted" @@ -251,9 +263,13 @@ docker logs 2>&1 | grep "OIDC" **Fix:** -1. Check server logs for `Email send failed`: +1. Check server logs for a failed send. The password-reset path and the generic notification path log different lines, so match both: ```bash - docker logs 2>&1 | grep "Email send failed" + docker logs 2>&1 | grep -E "Password reset email failed|Email send failed" + ``` + If neither matches, check whether the mail ever left at all — TREK needs a host, a port **and** a from-address (`SMTP_HOST` / `SMTP_PORT` / `SMTP_FROM`, or the same three fields under **Admin > Notifications**). With any one of them missing it skips SMTP entirely and logs `Password reset link issued (no SMTP)` plus the `===== PASSWORD RESET LINK =====` block instead of any error: + ```bash + docker logs 2>&1 | grep "no SMTP" ``` 2. If the error mentions TLS or certificate, set `SMTP_SKIP_TLS_VERIFY=true`. 3. Verify the port: `587` for STARTTLS, `465` for implicit TLS, `25` for plain SMTP. @@ -299,12 +315,22 @@ If `ALLOWED_ORIGINS` is not set, the default is **same-origin only** — cross-o ## Clipboard features not working (copy link, share, etc.) -**Cause:** The browser Clipboard API (`navigator.clipboard`) is only available in a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts). When accessing TREK over plain HTTP on a non-localhost address, the API is unavailable and clipboard operations silently fail or show an error. +**Cause:** The browser Clipboard API (`navigator.clipboard`) is only available in a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts), so on plain HTTP at a non-localhost address it is undefined. -**Fix:** The only supported options are: +TREK works around this where it matters most. The share-link and invite-link buttons in the trip **Members** dialog, the journey share link, and the calendar-subscribe URLs fall back to a hidden textarea plus the deprecated `document.execCommand('copy')`, which is not secure-context gated — **those keep working over plain HTTP**, on desktop and mobile alike. + +The remaining copy buttons call `navigator.clipboard` directly and have no fallback: + +- **Settings > Integrations (MCP)** — the MCP endpoint URL, the JSON client config, a newly created MCP token, and OAuth client IDs, client secrets and rotated secrets. These fail with no message at all, because the click handler throws before any toast is shown. +- **Settings > Account** — the 2FA backup codes. This one shows a generic error toast. Use the **Download** button next to it as a workaround; it does not need a secure context. +- **Admin Panel > Users & Invites** — the registration invite link, both on create ("create and copy") and via the copy button on an existing invite. + +**Fix:** For those buttons, one of: - Access TREK over HTTPS with a valid SSL certificate. -- Access TREK directly from `http://localhost:` — browsers treat `localhost` as a secure context for the Clipboard API (unlike the session cookie, which always requires HTTPS regardless of hostname). +- Access TREK directly from `http://localhost:` — browsers treat `localhost` as a secure context for the Clipboard API. + +Failing that, select the value shown in the field and copy it manually; every one of these buttons sits next to the text it copies. --- @@ -312,7 +338,7 @@ If `ALLOWED_ORIGINS` is not set, the default is **same-origin only** — cross-o **Cause:** When a Google Maps API key is set, TREK fetches photo references and image bytes from the Google Places API on the server side. If the server-side call is rejected or returns no photos, the `/place-photo/:id` endpoint answers `200 { "photoUrl": null }` and the place falls back to the default map-pin thumbnail. The image proxy behind it, `/place-photo/:id/bytes`, answers `204 No Content` when it has nothing cached — neither endpoint returns 404, so a trip full of photo-less places cannot trip a 404 rate limit in a reverse proxy or IPS. The most common causes are: -1. **HTTP referrer restriction on the API key.** Google Cloud Console lets you restrict a key to specific HTTP referrers. Because TREK calls Google from the server (not the browser), it sends a `Referer` header derived from `APP_URL`. If `APP_URL` is not set, the fallback is `http://localhost:`, which will not match any domain whitelist in GCP. +1. **HTTP referrer restriction on the API key.** Google Cloud Console lets you restrict a key to specific HTTP referrers. Because TREK calls Google from the server (not the browser), it sends a `Referer` header only when `APP_URL` is set — the header is the value of `APP_URL`. If `APP_URL` is not set, TREK sends no `Referer` header at all, and a referrer-restricted key rejects a request with no referrer just as it rejects a wrong one. 2. **Wrong key restriction type.** API keys restricted by **HTTP referrers** are designed for browser-side JavaScript. For a self-hosted server application, use **IP address** restrictions instead — add the public IP of your TREK server and no `APP_URL` configuration is needed. @@ -349,7 +375,7 @@ If the response is `{}` or `{"error": {...}}`, the key or its restrictions are b ## MCP OAuth flow does not initiate / "Connect" redirects but authentication never starts -**Cause:** TREK builds the OAuth 2.1 redirect URI from `APP_URL`. If `APP_URL` is not set, the authorization URL is constructed from a localhost fallback that external clients (Claude.ai, Claude Desktop) cannot reach, so the OAuth handshake never completes. +**Cause:** TREK advertises its OAuth 2.1 issuer and authorization endpoint from its resolved public base URL (`APP_URL`, else the first entry of `ALLOWED_ORIGINS`, else `http://localhost:`; the resolved value is kept only if it is `https://` or `localhost`/`127.0.0.1`). If that resolution lands on `http://localhost:`, external clients (Claude.ai, Claude Desktop) cannot reach the authorization endpoint and the OAuth handshake never completes. **Fix:** Set `APP_URL` to the public URL of your instance: @@ -360,7 +386,7 @@ environment: Restart the container after adding the variable. Once set, clicking **Connect** in the MCP client should redirect to your TREK instance and complete the OAuth flow normally. -> **Note:** `APP_URL` is required for any MCP OAuth integration. Without it, the authorization endpoint resolves to `http://localhost:`, which is unreachable from external MCP clients. +> **Note:** Set `APP_URL` for any MCP OAuth integration. TREK resolves its public base URL **once**, in this order: (1) `APP_URL`, (2) the first entry of `ALLOWED_ORIGINS`, (3) `http://localhost:` as a last resort — a step is skipped only when the value is unset or not a valid URL. The winner is then checked for MCP: only an `https://` URL or a `localhost` / `127.0.0.1` host is kept; anything else is replaced by `http://localhost:`, which external MCP clients cannot reach. Because only the resolved winner is checked, a valid but plain-HTTP `APP_URL` (e.g. `http://trek.internal.lan`) is **not** rescued by an `https://` entry in `ALLOWED_ORIGINS` — it still ends up on localhost. --- diff --git a/wiki/Weather-Forecasts.md b/wiki/Weather-Forecasts.md index 970d28a66..91fb655db 100644 --- a/wiki/Weather-Forecasts.md +++ b/wiki/Weather-Forecasts.md @@ -26,7 +26,7 @@ The Day Detail panel weather widget displays: | Date range | Data source | Cache TTL | |---|---|---| | Within 16 days (today −1 day to today +16 days) | Open-Meteo forecast API | 1 hour | -| More than 1 day in the past | Open-Meteo archive API (actual historical data) | 24 hours | +| More than 1 day in the past | Open-Meteo archive API (actual historical data), for the compact sidebar badge. The Day Detail panel asks the forecast API for past days too | 24 hours (1 hour in the Day Detail panel) | | More than 16 days in the future | Climate estimate from the same date in the prior year | 24 hours | Far-future estimates are labeled with a **Ø** prefix (e.g. "Ø 18 °C") to make it clear they are climate estimates, not a real forecast. The compact sidebar badge re-fetches silently in the background if a cached climate estimate could be upgraded to a live forecast.