docs(wiki): bring the wiki in line with 4.0.0 (#2034)

* docs(wiki): correct the install and operations pages for 4.0.0

Quick Start passed docker an empty ENCRYPTION_KEY: an assignment prefix is not
expanded on the same command line, so the server silently generated its own key
instead of taking the one the page told you to set. The tag examples pinned to
the 3.x line, the documented tmpfs size did not match the shipped compose file,
and several .env values were described as reaching the container when compose
never interpolates them.

* docs(wiki): match the MCP reference to the registered surface

The scope table was missing the whole Collections group and the totals said 27
scopes across 13 groups where the code registers 29 across 14. The Cloudflare
link on the setup page pointed at an anchor on its own page instead of the
heading on Troubleshooting.

* docs(wiki): correct the plugin and admin pages

Three links to Plugin-Development carried a .md suffix, which does not resolve on
the wiki. The admin pages described tabs, permissions and storage behaviour that
had moved on.

* docs(wiki): bring the feature pages back in line with the app

Costs still documented the old panel, where categories could be added, renamed,
reordered and deleted; they are fixed now and the panel is a day-grouped ledger
with settlements. Trip sharing, journeys, maps, packing, reservations and the
account pages had all drifted in the same way.

* docs(wiki): cover the pages the first pass did not reach

The MCP catalog listed 168 of the 201 registered tools; the settlement tools and
rate_place were missing here and the Collections family from the addon page.
Several resource rows claimed a trips:* scope where the handler asks for
trips:read.

* docs(wiki): tighten the corrections a review picked apart

The audit-log resource is the row id, not the credential id; an admin password
reset does not bump password_version the way the page implied; and a few of the
new paragraphs described behaviour more confidently than the code supports.

* docs(wiki): fix what a last read-through caught

Costs shows every figure in your display currency, not the trip's, and an
expense with no payer still counts against the members it is split between.
Studio's cutoff is a 1024px viewport rather than phones alone, and the
Integrations tab is gated on the providers an admin has configured.

* docs(wiki): the last few the review caught

A passkey satisfies the MFA policy everywhere except the WebSocket handshake,
which still asks for TOTP, so a passkey-only account under that policy gets no
live updates. Mobile optimizes a day from two places, not three. The ctx.db row
carried an unescaped pipe that split the table, and the Backups table linked
with a wiki link whose pipe did the same.
This commit is contained in:
Maurice
2026-08-25 12:36:52 +02:00
committed by GitHub
parent dc74d59b11
commit 7da0080557
77 changed files with 949 additions and 466 deletions
+13 -5
View File
@@ -4,7 +4,7 @@ The **AI Parsing** addon adds a large-language-model fallback to TREK's booking
It is an **opt-in addon, disabled by default**, and it works with a self-hosted local model, so no booking data has to leave your server.
> **Admin:** Enable **AI Parsing** in [Admin-Addons](Admin-Addons) (it sits in the *Integration* group). Booking import itself still requires the `kitinerary-extractor` binary — see [Reservations-and-Bookings](Reservations-and-Bookings#when-the-button-is-not-visible).
> **Admin:** Enable **AI Parsing** in [Admin-Addons](Admin-Addons) (it sits in the *Integration* group). Booking import then works even without the `kitinerary-extractor` binary — with no extractor present the model parses every uploaded file instead of only the ones Itinerary can't read. Installing the extractor is still the better setup: structured tickets stay fast and deterministic and the model remains a fallback. Configure a provider and model before pointing anyone at it, though: the **Import from file** button appears as soon as the addon is on, and with neither an extractor nor a configured model the import fails outright. See [Reservations-and-Bookings](Reservations-and-Bookings#import-from-booking-confirmation).
## How it fits with normal import
@@ -14,7 +14,7 @@ AI parsing does not replace [KDE Itinerary](Reservations-and-Bookings) — it ba
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.
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.
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.
## Choosing a provider
@@ -54,9 +54,11 @@ You can also select any other model already installed on the server, or type a m
If an admin leaves the instance config blank, each user can configure their own model under **Settings → Integrations → AI parsing** (the section only appears when the addon is enabled):
> *Use your own AI model to extract bookings from uploaded files. This applies only when your administrator has not configured a model for the whole instance.*
> *Choose the AI model used to extract bookings from uploaded files. This applies only when your administrator has not configured a model for the whole instance.*
The fields mirror the admin panel — provider, model, base URL (for local / OpenAI), and an API key that is *stored encrypted* (leave blank to keep the current key). There is also a **Send documents as images** toggle for vision-capable models.
The fields are a **Provider** (only **OpenAI** or **Anthropic** here), a **Model** id, and an **API key** that is *stored encrypted* (leave blank to keep the current key). There is no personal Base URL: the address this server calls is instance configuration, so a local (Ollama) model can only be set up by an admin on the addon, and the server answers 403 to anyone, admins included, who tries to store a personal base URL or a personal `local` provider.
There is also a **Send documents as images** toggle. It is stored per user, but extraction currently ignores it: only Anthropic is sent the raw PDF, every other provider always gets the extracted text.
> **Precedence:** an admin instance model always wins. Personal settings only take effect when no instance-wide model is configured.
@@ -64,12 +66,18 @@ The fields mirror the admin panel — provider, model, base URL (for local / Ope
The upload flow is the normal booking import — the AI simply runs behind it:
1. In the trip planner, open the **Reservations** tab and click **Import from file**.
1. In the trip planner, open the **Bookings** tab and click **Import from file**.
2. Drop your files (EML, PDF, PKPass, HTML, TXT — up to 5 files, 10 MB each) onto the upload area.
3. The upload dialog closes right away and a **background widget** (bottom-right) shows *Parsing files…* with a running count. You can keep navigating TREK while it works; the widget survives a page reload and even follows you to other pages.
4. When parsing finishes, click the widget's **Import** button to start the review.
5. Each parsed booking opens **pre-filled in the normal reservation (or transport) editor**, one at a time. Nothing is saved until you confirm each one.
### When nothing is found
If a job finishes with *No reservations could be extracted*, the widget offers a **Try AI parsing** button. It re-uploads the same files in force-AI mode: KDE Itinerary is skipped entirely and every file goes straight to the model. The offer only appears on an empty result, and never on a run that was already forced. If neither an instance-wide nor a personal model is configured, the retry fails with the error on the card instead of starting.
It is a retry, not a different route — on a normal import the model has already seen every file Itinerary returned nothing for, so it is worth pressing mainly when the addon or the model was configured after that job started. There is no way to push a document Itinerary *did* read through the model: as soon as the parse produced any booking at all, the widget shows **Import** and no retry button. Fix such a booking in the review editor instead.
### What gets filled in and created
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:
+5 -2
View File
@@ -31,10 +31,11 @@ Each sub-toggle can be disabled independently while the parent addon remains ena
### Global addons
Global addons add features that are not tied to a single trip. The default global addons are **Vacay**, **Atlas**, and **Journey**.
Global addons add features that are not tied to a single trip. The default global addons are **Vacay**, **Atlas**, **Collections**, and **Journey**.
- **Vacay** — personal vacation day planner with calendar view. Enabled by default.
- **Atlas** — world map of visited countries with travel stats. Enabled by default.
- **Collections** — personal place library: save places across trips into named lists, copy them into any trip, and share them. **Disabled by default.** See [Collections](Collections).
- **Journey** — trip tracking and travel journal (check-ins, photos, daily stories). **Disabled by default.**
**Sub-items on global addons:**
@@ -43,9 +44,11 @@ Global addons add features that are not tied to a single trip. The default globa
### Integration addons
Integration addons connect TREK to external services. Enabling an integration addon typically requires additional configuration (API keys, URLs) in the **Settings** tab.
Integration addons connect TREK to external services. Most of them need additional configuration (API keys, URLs) once enabled, but not in the admin **Settings** tab — each one has its own place.
- The **MCP** addon requires `APP_URL` to be set in your environment. When enabled, the **MCP Access** tab appears in the Admin Panel. **Disabled by default.** See [MCP-Overview](MCP-Overview) for full details.
- The **AirTrail** addon syncs flights from a self-hosted AirTrail instance. **Disabled by default.** The toggle here is the instance-wide switch only; each user connects their own instance (URL + API key) in **Settings → Integrations**.
- The **AI Parsing** addon is the LLM fallback for booking imports KItinerary cannot read. **Disabled by default.** When enabled, its provider, base URL, API key, and model fields appear inline underneath the addon row. Filling them in sets the instance-wide config for all users; leaving them blank lets each user configure their own provider in **Settings → Integrations**. See [AI-Booking-Import](AI-Booking-Import).
## Enabling or disabling an addon
+1 -1
View File
@@ -11,7 +11,7 @@ A category is a label consisting of a name, a color, and an icon. Users assign c
- In the place form's category selector
- As colored chips on place cards
- In the places filter panel
- In the map legend
- As the color of a place's map marker, and in the marker's hover popup
## Creating a category
+1 -1
View File
@@ -26,7 +26,7 @@ Below the support cards, a chronological timeline lists GitHub releases for the
- **Release date** and author
- A **Show details / Hide details** toggle that expands the release notes (Markdown rendered inline)
When the running server version is a stable release, pre-release entries are filtered out of the timeline.
Pre-release entries are hidden unless the server has both found a newer version and is itself running a pre-release. In every other case, including a pre-release install that is already on the newest pre-release, the timeline shows stable releases only.
Releases load 10 at a time. Click **Load more** at the bottom of the timeline to fetch additional pages.
+32 -4
View File
@@ -4,7 +4,7 @@ The Admin Panel is the central control surface for TREK instance operators. It i
## Accessing the Admin Panel
Navigate to the **Admin** link in the top navbar. If you do not see it, your account does not have admin privileges.
Open the user menu in the top navigation bar (your avatar), then select **Admin**. If the entry is not there, your account does not have admin privileges.
![Admin Panel](assets/AdminPanel.png)
@@ -18,11 +18,11 @@ The Admin Panel is divided into tabs. Most tabs are always visible; a few appear
| **Personalization** | Packing templates and place categories | No |
| **User Defaults** | Default settings applied to new users | No |
| **Addons** | Enable or disable optional features instance-wide | No |
| **Plugins** | Install, update, and manage plugins; rescan the plugins folder; review each plugin's capability audit. See [Admin-Plugins](Admin-Plugins) | No |
| **Plugins** | Install, update, and manage plugins; rescan the plugins folder; view each plugin's error log. See [Admin-Plugins](Admin-Plugins) | No |
| **Storage** | Storage backends, category assignment, replication, health | Hidden on managed instances |
| **Settings** | Authentication methods, MFA, allowed file types, API keys, OIDC/SSO configuration, and JWT secret rotation | No |
| **Notifications** | SMTP, webhook, ntfy, and push notification channel configuration; trip reminder toggle; admin notification preferences | No |
| **Backup** | Manual and scheduled database backups | Hidden on managed instances |
| **Backup** | Manual and scheduled full-instance backups: database, uploads, plugin data and plugin code. See [Backups](Backups) | Hidden on managed instances |
| **Audit** | Chronological activity log | No |
| **MCP Access** | OAuth sessions and static API tokens | Only when the MCP addon is enabled |
| **GitHub** | Release timeline and support links | Hidden on managed instances |
@@ -30,11 +30,39 @@ The Admin Panel is divided into tabs. Most tabs are always visible; a few appear
![Admin panel on the User Defaults tab, setting instance-wide defaults for colour mode, temperature unit, distance unit, time format, currency and blurred booking codes](assets/AdminUserDefaults.png)
### Linking to a tab directly
Every panel is reachable by URL, so a bookmark, an onboarding mail or a support reply can point at the one tab it is about instead of at the top of a page with thirteen of them:
```
/admin?tab=audit
```
Unlike the trip planner's `tab` parameter, this one stays in the address bar and is rewritten as you switch tabs, so the URL always says which panel you are looking at. It replaces the current history entry instead of adding one, so the back button still leaves the admin page rather than walking back through the tabs you visited.
| Tab | Id |
|-----|-----|
| Users | `users` (the default — carries no `?tab=`) |
| Personalization | `config` |
| User Defaults | `defaults` |
| Addons | `addons` |
| Plugins | `plugins` |
| Storage | `storage` |
| Settings | `settings` |
| Notifications | `notifications` |
| Backup | `backup` |
| Audit | `audit` |
| MCP Access | `mcp-tokens` |
| GitHub | `github` |
| Dev: Notifications | `dev-notifications` |
An id with no panel behind it opens **Users**, and on a managed instance the three tabs hidden there — `storage`, `github` and `backup` — fall back to **Users** as well. The other two conditional tabs behave differently: `mcp-tokens` and `dev-notifications` open their panel even when the MCP addon is off or the instance is not in development mode, with nothing highlighted in the sidebar, because the entry is missing from it.
## Plugin activity and audit
Plugins that are granted data-access capabilities have every host-mediated action they take recorded in a tamper-evident, hash-chained log. This log is separate from the instance **Audit** tab described above.
- **Admins** can review the per-plugin capability audit — every core-data read, broadcast, notification, and AI call a plugin made, with the acting user, the resource touched, and the outcome. This is exposed for each installed plugin in the admin plugin management view.
- **Admins** can review the per-plugin capability audit — every core-data read, broadcast, notification, and AI call a plugin made, with the acting user, the resource touched, and the outcome. It is served by `GET /api/admin/plugins/<id>/audit`; the admin plugin view itself currently surfaces only each plugin's error log (**View error log**).
- **Every user** (not just admins) can see the plugin actions taken in their own name under **Settings → Plugins**. This is what keeps a plugin's broad read grants accountable to the person whose data was read.
See [Audit-Log](Audit-Log) for details on the hash chain and how the two logs differ.
+8 -5
View File
@@ -29,7 +29,7 @@ Both views share a toolbar: a **Search plugins…** box, a **Type** filter (Widg
2. Click a card to open the **pre-install review dialog** (see below).
3. Click **Install**.
If the newest release requires a newer TREK than you are running, the button changes: when an older release still fits you get **Install {version}** for that one; when nothing fits, the button reads **Incompatible** and is disabled. Either way the dialog explains why in an amber note — it does not hide the reason behind a tooltip.
If the newest release does not support the TREK you are running — either it needs a newer one, or your TREK has outgrown the upper bound it declares (`>=3.2.0 <4.0.0` on TREK 4, say) — the button changes: when an older release still fits you get **Install {version}** for that one; when nothing fits, the button reads **Incompatible** and is disabled. Either way the dialog explains why in an amber note — it does not hide the reason behind a tooltip.
A newly installed plugin is **off**. Nothing runs until you enable it.
@@ -37,7 +37,7 @@ A newly installed plugin is **off**. Nothing runs until you enable it.
Before installing, read these sections:
- **What it can access** — the plugin's requested permissions, rendered as plain-language lines ("Read the trips…", "Create and edit places…"). Unknown permission codes appear verbatim. If it asks for nothing, this reads *Needs no special access.*
- **What it can access** — a plain-language summary of what the plugin's permissions let it do ("Reads your trips", "Edits places", "Provides photos"), plus a line for its own isolated database. It is a summary, not the raw permission list: permissions TREK has no summary line for — `notify:send`, `ai:invoke`, `jobs:run`, `db:write:members` and others — do not appear here at all, and a plugin that asks only for those still reads *Needs no special access.* For the complete list, read the manifest in the plugin's source repository. Only the consent dialog for an update renders permissions code by code, unknown codes verbatim.
- **Connects to** — every host the manifest declares it may reach, as monospace chips.
- **Setup** — settings the plugin will ask you (or each user) to fill in, tagged **Instance-wide** or **Per user**, and **Required** where applicable.
- **Details** — version, size, the TREK version range it requires, when it was reviewed, and total downloads.
@@ -48,7 +48,7 @@ A footer links to the **Source repository**, **Report an issue**, and the plugin
Some plugins talk to a service only *you* can name — a self-hosted Gotify, ntfy, or similar. Their manifest cannot list a host, so the review dialog adds a **+ hosts you add** chip and this note:
> *This plugin talks to a service only you can name (a self-hosted server). After installing, add the hosts it may reach under ⋯ → Allowed hosts. It can reach no others.*
> *This plugin talks to a service only you can name. After installing, add the hosts it may reach under ⋯ → Allowed hosts. It can reach no others.*
After install, open the row's **⋯ → Allowed hosts** dialog and add hostnames one at a time. Until you add at least one, the plugin's row shows an amber **Add allowed host** chip, because it can reach nothing and would otherwise look silently broken. Once hosts exist the chip turns blue and counts them.
@@ -56,10 +56,13 @@ Saving restarts the plugin so it picks up the new list — a running plugin's al
## Enabling, restarting, and disabling
Each installed row has a toggle (**Enable plugin**) and shows **Active** or **Off**. A coloured dot on the icon tile reflects runtime health: active, starting, error, inactive, disabled, or incompatible.
Each installed row has a toggle (**Enable plugin**) and shows **Active** or **Off**. A coloured dot on the icon tile reflects runtime health: green when the plugin is active, a pulsing blue while it starts, red when it has errored, grey when it is not running. Incompatibility is not a dot colour — a plugin this TREK cannot run gets an amber chip on the row instead.
Enabling can be refused for good reasons, each of which opens the right remedy:
Enabling can be refused for good reasons. The first three are version gates that run before everything else — a plugin that cannot run on this TREK is never offered a permission dialog, because consenting would not make it start:
- **This TREK is outside the range the plugin declares** — the row carries an amber *Needs TREK {range} — this server runs {host}* chip and the toggle is refused. Expect this after a major upgrade: a plugin that declared `>=3.2.0 <4.0.0` stops activating on TREK 4 even though it ran fine the day before. There is no dialog to click through, it takes a release from the author that admits this TREK — and once the registry has one, the row's **Update** button offers it.
- **The plugin does not say which TREK versions it supports** — the chip reads *Does not say which TREK versions it supports*. That is a plugin folder which predates the range field or was dropped into the plugins directory by hand: **Rescan** registers it rather than letting it vanish silently, and the gate then refuses it instead of guessing. It needs a manifest with a `trek` range.
- **The plugin needs a newer plugin API** — its manifest's `apiVersion` is higher than the plugin API this TREK implements (currently v1). Install, upload, and dev-link reject that up front, so this only shows for a plugin picked up from the plugins directory on disk — registered at the next restart, or when you press **Rescan**.
- **A required addon is disabled** — a toast names the addon; turn it on in [Admin-Addons](Admin-Addons).
- **A plugin dependency is missing or outdated** — a dialog lists each dependency with a one-click **Download** / **Update** that installs the newest compatible version and then retries.
- **The update widened its permissions** — the consent dialog (below).
+6 -3
View File
@@ -12,7 +12,7 @@ The user table shows every registered account with the following columns:
|--------|-------------|
| **User** | Avatar, username, and an always-visible presence dot (green = online, grey = offline) |
| **Email** | Account email address |
| **Role** | Badge showing `admin` or `user` |
| **Role** | Badge showing **Administrator** or **User** |
| **Created** | Account creation date |
| **Last Login** | Date and time of most recent login |
| **Actions** | Edit and delete buttons |
@@ -27,11 +27,13 @@ Click the pencil icon on any row to open the edit form. You can change:
- **Username**
- **Email address**
- **Role** — toggle between `user` and `admin`
- **Role** — a dropdown with **User** and **Administrator**
- **Password** — set a new password; must be at least 8 characters
Click **Save** to apply changes.
Below the fields sits **Reset passkeys**, which removes every passkey that user has registered — the recovery path when someone loses their authenticator. It asks for confirmation, reports how many passkeys it removed, and takes effect immediately rather than on **Save**. The user can still sign in with their password. See [Passkeys](Passkeys).
### Delete a user
Click the trash icon and confirm. Deletion is permanent. The user's account is removed from the database along with their data (cascade behavior is enforced at the database level).
@@ -50,7 +52,7 @@ Invite links let a specific number of people register themselves. This is useful
### Creating an invite
Click **Create Invite** (invite links section, below the user table). Configure:
Click **Create Link** (invite links section, below the user table). Configure:
- **Max uses** — how many times the link can be used before it expires: `1×`, `2×`, `3×`, `4×`, `5×`, or `∞` (unlimited). Defaults to `1×`.
- **Expiry** — how long the link remains valid: `1d`, `3d`, `7d`, `14d`, or `∞` (no expiry). Defaults to `7d`.
@@ -87,4 +89,5 @@ The **Users** tab also hosts the Permissions panel at the bottom, which controls
- [Invite-Links](Invite-Links)
- [Admin-Permissions](Admin-Permissions)
- [Two-Factor-Authentication](Two-Factor-Authentication)
- [Passkeys](Passkeys)
- [Admin-Panel-Overview](Admin-Panel-Overview)
+10 -1
View File
@@ -8,7 +8,7 @@ Tune how TREK looks — colour mode, accent scheme, readability, and which dashb
Open **Settings** and select the **Appearance** tab.
Changes preview instantly and are saved to your account a moment later, so they follow you to every browser you sign in with. A **Reset to defaults** button at the bottom of the tab restores everything on this page in one click.
Changes preview instantly and are saved to your account a moment later, so they follow you to every browser you sign in with. A **Reset to defaults** button at the bottom of the tab restores the colour scheme, the readability settings, the dashboard widgets and the mobile layout in one click. Your **Color mode** (Light / Dark / Auto) is a separate setting and is left as it is.
> These are personal settings — no permission and no admin toggle gate them.
@@ -77,6 +77,15 @@ Each of the four rows renders a live sample in that tier ("Large heading", "Medi
Hiding a widget here only affects your own dashboard. Widgets that depend on an addon (for example **Collections**) also need that addon enabled by an admin — see [Dashboard-Widgets](Dashboard-Widgets) and [Admin-Addons](Admin-Addons).
## On a phone
Opened on a phone, the Appearance screen carries an extra **Mobile** card that the desktop tab does not have:
- **Bottom navbar** — which items sit in the bottom bar, in which order, and which are demoted under **More**. **Dashboard** is pinned first and cannot be moved; two more items fit next to it, and a counter above the list shows how full the bar is. Leaving both lists untouched keeps TREK's automatic split.
- **Dashboard order** — reorder how the trip list and the inline widgets stack on the phone dashboard. The featured trip always stays on top. A block whose widget is switched off under **Dashboard widgets** keeps its place in the list and is marked **Hidden**.
Both are stored on your account alongside the rest of the appearance settings, and both are cleared by **Reset to defaults**.
## Permissions
None. Every setting on this tab applies only to your own account.
+40 -2
View File
@@ -21,6 +21,11 @@ Actions are grouped by area below. The **Action key** is the raw value stored in
| `user.login_failed` | Login attempt failed |
| `user.password_change` | User changed their password |
| `user.account_delete` | User deleted their account |
| `user.password_reset_request` | Password reset requested |
| `user.password_reset_success` | Password reset completed |
| `user.password_reset_fail` | Password reset attempt rejected (`reason` in details) |
A request for an account that can actually be reset writes **two** rows: one with `delivered: "pending"` when the mail is handed off, one with the delivery result. Every other outcome writes a single row carrying a `reason` instead — `no_user`, `oidc_only`, `throttled_per_email` or `password_login_disabled`. Passkey logins are not a separate key: they land as `user.login` with `method: passkey` in the details.
### MFA
@@ -29,6 +34,14 @@ Actions are grouped by area below. The **Action key** is the raw value stored in
| `user.mfa_enable` | MFA enabled on an account |
| `user.mfa_disable` | MFA disabled on an account |
### Passkeys
| Action key | Description |
|---|---|
| `user.passkey_register` | Passkey enrolled |
| `user.passkey_delete` | Passkey removed (resource = the passkey's numeric ID) |
| `user.passkey_clone_suspected` | A passkey presented a signature counter that did not advance — a possible cloned authenticator. That assertion is rejected, the credential stays enabled |
### Trips
| Action key | Description |
@@ -37,6 +50,10 @@ Actions are grouped by area below. The **Action key** is the raw value stored in
| `trip.update` | Trip updated (includes changed fields) |
| `trip.copy` | Trip duplicated (includes source and new trip IDs) |
| `trip.delete` | Trip deleted (includes trip ID and title) |
| `trip.transfer_ownership` | Trip ownership transferred (includes the trip title and both owner emails) |
| `trip.invite_link_create` | Trip invite link created or rotated (a trip has one link, so creating it again replaces the previous token) |
| `trip.invite_link_delete` | Trip invite link revoked |
| `trip.invite_link_join` | An invite link was accepted (`joined` in details). The row is written even when nobody was added — the trip owner or an existing member opening the link logs `joined: false` |
### Admin actions
@@ -45,22 +62,41 @@ Actions are grouped by area below. The **Action key** is the raw value stored in
| `admin.user_create` | User created by admin |
| `admin.user_update` | User edited by admin (role, email, username, etc.) |
| `admin.user_delete` | User deleted by admin |
| `admin.user_mfa_reset` | A user's MFA reset by admin |
| `admin.user_passkeys_reset` | All of a user's passkeys removed by admin |
| `admin.invite_create` | Invite link created |
| `admin.invite_delete` | Invite link deleted |
| `admin.permissions_update` | Instance permissions updated |
| `admin.oidc_update` | OIDC/SSO settings updated |
| `admin.addon_update` | Addon enabled, disabled, or configured |
| `admin.oauth_session.revoke` | OAuth session revoked by admin |
| `admin.oauth_session_revoke` | OAuth session revoked by admin |
| `admin.mcp_token_delete` | MCP token revoked by admin |
| `admin.rotate_jwt_secret` | JWT secret rotated |
| `admin.bag_tracking` | Bag tracking feature toggled |
| `admin.places_photos` | Places photos feature toggled |
| `admin.places_autocomplete` | Places autocomplete feature toggled |
| `admin.places_details` | Places details feature toggled |
| `admin.places_enrich` | Place enrichment feature toggled |
| `admin.collab_features` | Collaboration features updated |
| `admin.packing_template_create` | Packing template created |
| `admin.packing_template_delete` | Packing template deleted |
| `admin.plugin_retrust` | Plugin signing key re-trusted (records the old and the new key fingerprint) |
| `admin.storage_update` | Storage configuration saved (secrets redacted in the details) |
| `admin.storage_test` | Storage backend probed (the probe writes and deletes one test object) |
| `admin.storage_backfill` | Replica catch-up started |
| `admin.storage_backfill_cancel` | Replica catch-up cancelled |
| `admin.storage_migration` | Category migration started |
| `admin.storage_migration_cancel` | Category migration cancelled |
| `admin.storage_stats_refresh` | Storage usage stats recomputed |
| `admin.default_user_settings_update` | Default user settings updated |
| `admin.demo_baseline_save` | Demo baseline snapshot saved |
### Settings
| Action key | Description |
|---|---|
| `settings.app_update` | App settings updated (SMTP, webhooks, MFA policy, etc.) |
| `settings.api_keys_update` | Maps / OpenWeather / Unsplash API keys changed. Written for any user saving their own keys; an admin's save also updates the instance-wide ones. Records the changed key **names** only, never the values |
### Backups
@@ -91,19 +127,21 @@ Actions are grouped by area below. The **Action key** is the raw value stored in
| `oauth.token.revoke` | OAuth token revoked |
| `oauth.token.grant_failed` | OAuth token grant attempt failed |
| `oauth.token.client_auth_failed` | OAuth client authentication failed |
| `oauth.token.replay_detected` | An already-revoked refresh token was replayed. The whole token chain and the user's OAuth sessions for that client are revoked. Two clients racing on the same token do **not** land here: that is logged as `oauth.token.refresh` with `concurrent: true` |
### Integrations
| Action key | Description |
|---|---|
| `immich.private_ip_configured` | Immich URL saved that resolves to a private IP |
| `airtrail.private_ip_configured` | AirTrail URL saved that resolves to a private IP |
## Log columns
| Column | Description |
|---|---|
| Time | Timestamp of the action |
| User | Username and email of the acting user (or `anonymous` for unauthenticated events) |
| User | Username of the acting user, falling back to their email, then `#<user id>`. Unauthenticated events show `—` (the plain-text log file writes `anonymous` for those instead) |
| Action | Action key (see tables above) |
| Resource | Affected resource (filename, trip ID, tool name, etc.) where applicable |
| IP | Client IP address |
+7 -5
View File
@@ -15,7 +15,7 @@ A backup is a ZIP archive with these entries:
| Entry | Contents |
|---|---|
| `travel.db` | The full SQLite database |
| `uploads/` | All uploaded attachments, covers, and avatars (default location — see [[Admin: Storage|Admin-Storage]]) |
| `uploads/` | All uploaded attachments, covers, and avatars (default location — see [Admin: Storage](Admin-Storage)) |
| `plugins-data/` | Each installed plugin's own database + files (present only if plugins are installed) |
| `plugins-code/` | The installed plugin code, so a restore is self-contained (dev-linked plugins are skipped) |
@@ -32,7 +32,9 @@ You can also download or delete any existing backup from the list.
You can restore from:
- **A stored backup** — click **Restore** next to any backup in the list.
- **An uploaded ZIP** — click **Upload & Restore** and select a backup file from your computer (maximum upload size: 500 MB by default, configurable with the `BACKUP_UPLOAD_LIMIT_MB` environment variable — see [Environment-Variables](Environment-Variables)).
- **An uploaded ZIP** — click **Upload Backup** and select a backup file from your computer (maximum upload size: 500 MB by default, configurable with the `BACKUP_UPLOAD_LIMIT_MB` environment variable — see [Environment-Variables](Environment-Variables)).
A second limit caps the **decompressed** size of a restore archive at 5 GB by default (`BACKUP_MAX_DECOMPRESSED_MB`). It applies to both routes — a stored backup as well as an uploaded ZIP — and a restore that exceeds it is refused with `Backup exceeds the maximum decompressed size.` Raise it if your own backups legitimately grow past the default, otherwise they become unrestorable.
Before restoring, TREK runs integrity checks on the uploaded database:
@@ -41,7 +43,7 @@ Before restoring, TREK runs integrity checks on the uploaded database:
> **Warning:** Restoring replaces all current data. Back up your current state first if you want to keep it.
> **Plugins & restart:** `travel.db` and `uploads/` are swapped in immediately. Plugin data and code are **staged** and applied on the **next server restart** — the running plugins hold their databases open, so they can't be swapped live (the same reason the bundled encryption key only takes effect on restart). Restart the server after restoring an instance that uses plugins.
> **Plugins & restart:** `travel.db` and `uploads/` are swapped in immediately. Plugin data and code are **staged** beside the live trees and applied right away: the running plugins hold their databases open, so the restore asks the plugin runtime to shut them down and swaps the trees in the moment the handles are closed. If the runtime isn't up (plugins switched off, or a restore that happens during startup), the staged trees are applied at the next boot instead. Restart the server after restoring an instance that uses plugins — the plugins are stopped for the swap and stay down until the process restarts, and the bundled encryption key is only read at startup.
## Auto-backup
@@ -54,7 +56,7 @@ Enable scheduled backups in the **Auto-Backup** section of the Backup tab.
- Weekly
- Monthly
**Retention** (`Keep last … days`) — enter a number of days. Backups older than that many days are pruned after each auto-backup run. Set to **0** to keep all backups indefinitely (no pruning).
**Retention** (`Delete old backups after`) — pick one of the preset windows: 1 day, 3 days, 7 days, 14 days, 30 days, or **Keep forever**. Auto-backups older than the chosen window are pruned after each auto-backup run; **Keep forever** (stored as `keep_days = 0`) keeps all backups indefinitely.
**Schedule** options (depend on interval):
@@ -84,7 +86,7 @@ The following actions are recorded in the [Audit-Log](Audit-Log):
## See also
- [[Admin: Storage|Admin-Storage]] — replicate backups to S3-compatible storage
- [Admin: Storage](Admin-Storage) — replicate backups to S3-compatible storage
- [Encryption-Key-Rotation](Encryption-Key-Rotation)
- [Admin-Panel-Overview](Admin-Panel-Overview)
- [Security-Hardening](Security-Hardening)
+45 -37
View File
@@ -24,40 +24,36 @@ Costs is **multi-currency** (#551). Three settings are involved, and they do dif
- Each **expense** carries **its own currency** — pick it in the expense modal and enter what the receipt says (a $100 dinner on a rouble trip is `100 USD`). It is converted into the trip currency at a rate **frozen when you save it**, so a settled debt doesn't reopen when the market moves.
- Your **display currency** (Settings → General) converts what you *read* — totals, chart, balances — into one currency. It changes nothing that is stored. Left on **Trip currency** (the default), each trip is shown in its own currency.
165 currencies are supported, with rates from [Frankfurter](https://frankfurter.dev) (no API key needed). When an item's currency differs from the display currency, the modal shows the converted amount alongside the rate (`1 {from} in {to}`), and the ledger row shows both (`$100.00 → 7 668,71 ₽`).
165 currencies are supported, with rates from [Frankfurter](https://frankfurter.dev) (no API key needed). When an item's currency differs from the display currency, the modal shows the original next to the converted amount (`$100.00 ≈ 7 668,71 ₽ · live rate`), and the ledger row shows both (`$100.00 → 7 668,71 ₽`).
> **Read [Currencies](Currencies) for the full picture** — how the three interact, what happens when you change a trip's currency, and which currency a public share link is shown in.
## Categories
Expenses are grouped into categories. Each category is shown with a small colored square indicator that cycles through a 12-color palette as you add more categories.
Every expense sits in one of **14 fixed categories**. You cannot add, rename, reorder or delete them — you pick one from a row of pills in the expense modal. Each category carries its own icon and colour, which is what the coloured tab on an expense row and the swatches in the sidebar breakdown are showing:
From the toolbar you can:
**Accommodation**, **Food & drink**, **Groceries**, **Transport**, **Flights**, **Activities**, **Sightseeing**, **Shopping**, **Fees & tickets**, **Health**, **Tips**, **Fuel**, **Parking**, **Other**.
- **Add a category** — type a name and click the **+** button (or press Enter).
- **Rename a category** — click the pencil icon next to its name in the category header.
- **Reorder categories** — drag the grip handle on the left of the category header.
- **Delete a category** — click the trash icon in the category header. This deletes all expense items inside it.
Expenses written before the Costs rework carried free-text categories. Those are matched onto the fixed keys where the stored label is recognisable (a booking saved as `Flight` reads as **Flights**, `gas` as **Fuel**) and fall back to **Other** otherwise. Nothing is rewritten in the database, only how the row is displayed.
## Expense items
Each category contains a table of items with the following columns:
Expenses are listed as a ledger grouped by **day**, newest day first, with the day's total on the right of each group header. Recorded settle-up payments sit in the same list, so the ledger reads as everything that happened on the trip's money in order. A row shows the category as a coloured tab, the name, the payer chips with what each of them put in, the note, and the total in your display currency — plus a green **you lent** / red **you borrowed** chip under the amount when the split leaves you up or down on it. An expense entered in another currency shows the original and the converted amount under the name.
| Column | Notes |
Above the list are a search box, a category filter, a day filter, an **All / Paid by me / I'm owed** switch and the CSV export button.
Click **Add expense**, or the pencil beside a row, to open the expense editor:
| Field | Notes |
|---|---|
| Name | Editable inline. Read-only when linked to a reservation. |
| Total | The total cost for this item. |
| Persons | Number of persons (or member chips on multi-member trips). |
| Days | Number of days. |
| Per Person | Calculated: Total ÷ Persons. |
| Per Day | Calculated: Total ÷ Days. |
| Per Person/Day | Calculated: Total ÷ (Persons × Days). |
| Date | Optional expense date. |
| Note | Free-text note. |
Click any editable cell to edit it inline. Drag the grip handle to reorder items within a category.
Add a new item using the inline **add row** at the bottom of each category table.
| What was it for? | The expense name. Required — the dialog will not save without it. |
| Total amount | What the receipt says. In Ticket mode it is summed from the items instead and cannot be typed. |
| Currency | The expense's own currency. |
| Day | Optional expense date. Undated expenses group under **No date**. |
| Category | One of the 14 fixed categories. |
| Who paid? | Who actually put the money down — see [Who paid](#who-paid). |
| Split | How the total is shared out — see [Splitting costs](#splitting-costs). |
| Note | Free-text note, shown on the row. |
### Expenses linked to a booking or a place
@@ -65,26 +61,34 @@ An expense can hang off a **booking** (reservation or transport) or off a **plac
Deleting the booking or the place deletes its linked expense with it. Removing the expense from the record's Costs block deletes only the expense and leaves the record standing.
## Who paid
**Who paid?** in the expense editor records who actually put the money down. It is the other half of the settlement maths — the split says who owes for the expense, this says who is out of pocket for it:
- **One person paid** — pick a member from the dropdown. This is the default.
- **Multiple people paid** — switch with the link beside the label, include each payer and type what each of them put in. The amounts have to add up to the total; until they do, the dialog reads *Payer amounts must add up to …* and refuses to save.
- **No one paid yet** — the first entry in the single-payer dropdown, for an expense you are only planning. The amount still counts toward **Total trip spend**, and everyone on its split is still charged their share in **Balances** — what is missing is the credit side, so nobody is recorded as having covered it.
An expense with no payer is flagged **Unfinished** on its row and counted into the **Outstanding amount** card, which is where to look when the balances show money owed that nobody is down as having paid.
## Splitting costs
The **Persons** column behaves differently depending on the trip:
**Split** decides who owes for the expense. Every trip member is listed and can be included or excluded, and there are three modes:
- **Single-user trip** — enter a number of persons directly.
- **Multi-member trip** — a member chip picker appears. Click the edit button to open the expense modal, where you can select:
- **Equally** — Splits the cost equally among selected members. Remainder cents (from rounding errors) are distributed deterministically and rotated using the item ID to ensure everyone is charged equally over the course of the trip.
- **Custom** — Enter specific custom amounts for each traveler. The sum of the custom splits must balance exactly to the total price.
- **Ticket** — Build an itemized list of expenses (e.g. Apples: $10, cake: $50, Milk: $40) and assign specific trip participants to split each individual item. Individual shares are calculated cent-perfectly, the total expense price is automatically summed, and the list of itemized splits is saved/restored across edits.
Click an assigned member chip again to mark them as **paid** (the chip shows a green ring).
- **Equally** — Splits the cost equally among selected members. Remainder cents (from rounding errors) are distributed deterministically and rotated using the item ID to ensure everyone is charged equally over the course of the trip.
- **Custom** — Enter specific custom amounts for each traveler. The sum of the custom splits must balance exactly to the total price.
- **Ticket** — Build an itemized list of expenses (e.g. Apples: $10, cake: $50, Milk: $40) and assign specific trip participants to split each individual item. Individual shares are calculated cent-perfectly, the total expense price is automatically summed, and the list of itemized splits is saved/restored across edits.
![Add Expense](assets/BudgetAddExpensive.gif)
## Settlement calculator
When multiple members are assigned to expenses and there are outstanding debts between members, a collapsible **Settlement** section appears inside the total card. Click the section header to expand it. It shows the minimum number of transfers needed to settle all debts (using a greedy matching algorithm), including:
Costs works out the minimum number of transfers needed to settle all debts (using a greedy matching algorithm) and keeps the answer in the right-hand column, split across two cards:
- Transfer flows: who pays whom and how much.
- Net balances: each member's overall surplus or deficit.
- **Settle up** — the transfer flows: who pays whom and how much. The number of open flows sits in the card header, and each flow has a **Settle** button that records it as done.
- **Balances** — net balances: each member's overall surplus or deficit.
**Settle up** in the panel header records every open flow at once. **Add payment** on the card records a single transfer by hand, for a repayment that did not follow a suggested flow. Recorded payments then appear in the expense ledger as their own rows, with edit and undo beside them.
Balances are always netted in the **trip currency** and converted to your display currency once, at the end — so they stay stable even when the trip mixes currencies.
@@ -96,10 +100,14 @@ A recorded payment carries **its own currency** too: settling a rouble debt with
## Costs summary
The right-hand column contains two widgets:
Four cards sit above the expense list:
- **Total card** — displays the grand total in large type. On multi-member trips it also shows a per-member breakdown with a proportional bar.
- **Donut chart** — spending by category. Each segment uses that category's color. The legend always shows the amount and percentage for each category; hovering a legend row highlights it.
- **You owe** — what you still have to transfer, with the members you owe it to.
- **You're owed** — the same in the other direction.
- **Outstanding amount** — the total of the expenses that have no payer yet, and how many of them there are.
- **Total trip spend** — the grand total in large type, with your share and what you paid underneath.
Below the settle-up and balances cards, the right-hand column ends with **By category**: spending per category as a ranked list of bars in the category colours. Only categories with spend on them appear, sorted by amount, and the bars are scaled against the biggest category rather than the trip total, so the ranking stays readable.
## Exporting
@@ -107,7 +115,7 @@ Click **Export CSV** in the toolbar to download all expenses as a spreadsheet (r
## Permissions
All write operations (adding/editing/deleting items and categories, and an expense's currency) require the `budget_edit` permission. The **trip** currency lives on the trip itself, so changing that requires `trip_edit` instead.
All write operations (adding/editing/deleting expenses and settle-up payments, and an expense's currency) require the `budget_edit` permission. The **trip** currency lives on the trip itself, so changing that requires `trip_edit` instead.
## See also
+4 -3
View File
@@ -2,13 +2,13 @@
Subscribe your calendar app to a TREK trip so it stays in sync automatically, instead of importing a snapshot once.
> **Not the same as the ICS export.** The **Download ICS** action described in [Day-Plans-and-Notes](Day-Plans-and-Notes) writes a one-off `.ics` file that never changes after you import it. A calendar *feed* is a live URL your calendar re-fetches on its own. Use the export for a frozen copy, a feed for something that keeps up with your edits.
> **Not the same as the ICS export.** The **Download .ics** action described in [Day-Plans-and-Notes](Day-Plans-and-Notes) writes a one-off `.ics` file that never changes after you import it. A calendar *feed* is a live URL your calendar re-fetches on its own. Use the export for a frozen copy, a feed for something that keeps up with your edits.
## Where to find it
There are two feeds, reached from two places:
- **Per-trip feed** — in the trip planner, hover the **ICS** button in the Day Plan sidebar toolbar and choose **Subscribe to calendar**. (The other entry in that menu, **Download ICS**, is the one-off export.)
- **Per-trip feed** — in the trip planner, click the **Export** button in the Day Plan sidebar toolbar and pick **Subscribe to calendar** under **Calendar**. (The entry above it, **Download .ics**, is the one-off export.) The subscribe entry only shows up if you may manage the trip's share links — see *Permissions* below.
- **All-trips feed** — on the **My Trips** dashboard, click the calendar-plus button in the toolbar (**Subscribe to all trips**).
Both open the same dialog.
@@ -61,12 +61,13 @@ The feed carries the same events as the ICS export:
- **Reservations** — hotels, restaurants, and transport. Flights and other transport take their start and end from the departure and arrival endpoints, each in its own time zone. Reservations with no placeable date are skipped.
- **Accommodations** — an all-day event covering every night of the stay, from the arrival day to the departure day, so a hotel sits above those days rather than appearing once on the day you check in. The dates come from the trip days the stay is attached to, so reordering days moves the event with them.
- **Check-in and check-out** — separate timed events on the arrival and departure days whenever the stay records those times. If you entered a check-in window, its end becomes the event's end time.
- **Car pickup and drop-off** — a reservation of type *Car* also gets two standalone timed events, *Pickup: {title}* and *Drop-off: {title}*, on top of the rental's own booking block, both carrying the reservation's location. Each side prefers its own endpoint — the pickup endpoint for the pickup, the return endpoint for the drop-off — and takes that endpoint's local time and time zone. Otherwise it falls back to the booking's own times (an end value stored as a bare clock is paired with the booking's start date) and to the time zone of the linked place. A side with no usable date and time from either source is skipped, so a rental imported with only one geocoded endpoint can end up with just one marker.
Feeds are served with cache headers that tell clients not to cache, plus an hourly refresh hint (`REFRESH-INTERVAL` / `X-PUBLISHED-TTL` of one hour). Most calendar apps treat that as a suggestion — Google in particular refreshes on its own schedule, often much slower — so an edit may take a while to show up.
## Permissions
No dedicated permission gates feeds. You can manage a per-trip token if you own the trip or are a member of it; anyone else gets a *Trip not found*. The all-trips feed is always scoped to your own account.
Managing a per-trip token requires the **`share_manage`** permission — the same right that governs invite and share links. By default it sits with the trip owner; an instance can lower it to trip members in [Admin-Permissions](Admin-Permissions). A member without it never sees **Subscribe to calendar** in the export dialog and gets a *No permission* from the token endpoint, while anyone with no access to the trip at all still gets a *Trip not found*. The all-trips feed needs no permission — it is always scoped to your own account.
Because the token grants unauthenticated read access, enabling a feed effectively shares that trip's contents with whoever holds the link, regardless of trip roles — see [Public-Share-Links](Public-Share-Links) for the equivalent trade-off on the sharing side.
+1 -1
View File
@@ -14,7 +14,7 @@ The Collab addon must be enabled by an admin and the Chat sub-feature must be tu
Type in the input field at the bottom and press **Enter** (or click the send button) to post. Hold **Shift + Enter** to insert a line break without sending.
Messages load in pages of 100. A **Load more** button appears at the top of the chat when older messages are available.
Messages load in pages of 100. A **Load older messages** button appears at the top of the chat when older messages are available.
## Emoji
+7 -1
View File
@@ -47,7 +47,13 @@ Click the pin icon on a note card to pin it. Pinned notes sort to the top of the
You can attach images, PDFs, and other files to a note (requires the `file_upload` permission). Maximum file size is **50 MB** per file.
The following file types are blocked: `.svg`, `.html`, `.htm`, `.xml`, `.xhtml`, `.js`, `.jsx`, `.ts`, `.exe`, `.bat`, `.sh`, `.cmd`, `.msi`, `.dll`, `.com`, `.vbs`, `.ps1`, `.php`.
The following file types are blocked:
- Markup a browser would render: `.svg`, `.svgz`, `.html`, `.htm`, `.shtml`, `.shtm`, `.xml`, `.xhtml`, `.xht`
- Scripts: `.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs`, `.php`, `.py`, `.rb`, `.pl`
- Executables: `.exe`, `.bat`, `.sh`, `.cmd`, `.msi`, `.dll`, `.com`, `.vbs`, `.ps1`, `.app`
An attachment is also rejected when its MIME type contains `svg`, `html`, or `javascript`, whatever its extension says. The same blocklist guards the trip file manager (see [Documents-and-Files](Documents-and-Files)), where uploads must additionally match the admin's **Allowed File Types** setting (video files are exempt from that list) — note attachments are not subject to it at all.
Image thumbnails are shown on the note card. Click a thumbnail to open a lightbox. PDFs open in a document viewer overlay.
+5 -4
View File
@@ -10,21 +10,22 @@ Open the trip planner → **Collab** tab → **Polls** section. The Collab addon
## Creating a poll
Users with `collab_edit` permission can click **+ New** in the Polls header. A modal opens with the following fields:
Users with `collab_edit` permission can click **+ New Poll** in the Polls header. A modal opens with the following fields:
| Field | Required | Notes |
|-------|----------|-------|
| Question | Yes | The poll prompt |
| Options | Yes | At least 2 options required; add more with **+ Add option** |
| Multiple choice | No | Toggle on to allow voters to select more than one option |
| Deadline | No | Mobile only; date and time after which voting closes automatically |
Click **Create** to save and broadcast the poll to all connected members.
Click **Create Poll** to save and broadcast the poll to all connected members.
## Voting
Click an option button to vote. A filled circle and blue highlight indicate your selection. For **multiple-choice** polls, you can click additional options to select more than one; clicking an already-selected option removes your vote for that option. For **single-choice** polls, clicking a different option moves your vote to the new selection.
Percentage bars and voter avatars are only visible **after you have voted** or once the poll is closed or expired.
Voter avatars and the numeric percentage are only visible **after you have voted** or once the poll is closed or expired. The fill bar behind each option is always drawn, in a faint neutral tint until you vote, and the total vote count in the poll header is visible to everyone — so the rough distribution can be read before you cast your vote.
## Results
@@ -38,7 +39,7 @@ The option with the most votes is highlighted when the poll is closed or expired
## Deadline
Polls may have an optional deadline set via the API. When a deadline is present, a live countdown badge appears on the poll card, updating every **30 seconds**. The badge shows the remaining time in days/hours/minutes format.
Polls may carry an optional deadline. On **mobile** the new-poll sheet has a **Deadline** field — a date-and-time picker with a clear button. The desktop create modal does not offer one yet, so on desktop a deadline can only be set through the API or MCP. When a deadline is present, a live countdown badge appears on the poll card, updating every **30 seconds**. The badge shows the remaining time in days/hours/minutes format.
When the deadline passes, the poll is automatically treated as closed: voting is disabled and results are displayed to everyone.
+1 -1
View File
@@ -14,7 +14,7 @@ A place you save to a trip only exists inside that trip. Collections is the oppo
When the admin has enabled the addon, a **Collections** entry appears in the main navigation (and a bottom-tab on mobile). Opening it shows your lists on the left, the active list's places in the middle, and a map on the right.
The dashboard also gains a **Collections widget** that surfaces your lists as compact badges. Each user can hide that widget from their own dashboard under [Display Settings](Display-Settings) without affecting anyone else.
The dashboard also gains a **Collections widget** that surfaces your lists as compact badges. Each user can hide that widget from their own dashboard under **Dashboard widgets** on the Appearance tab (see [Appearance-Settings](Appearance-Settings)) without affecting anyone else.
## Lists
+11 -9
View File
@@ -52,12 +52,14 @@ See the [[Development Environment|Development-environment]] page for the full se
## Tech Stack
| Layer | Technology |
|---|---------------------------------------------------------------------------------|
| Frontend | React 18, TypeScript, Zustand, Leaflet, Tailwind CSS, Vite |
| Backend | Express, TypeScript, better-sqlite3 |
| Real-time | WebSocket (ws) |
| Database | SQLite with WAL mode |
| Auth | JWT (HS256), bcrypt, TOTP MFA, OIDC |
| Maps | Leaflet + react-leaflet, OSRM, Nominatim, CartoDB tiles |
| i18n | 15 languages (EN, DE, ES, FR, NL, IT, PT-BR, CS, PL, HU, RU, ZH, ZH-TW, AR, ID) |
| Layer | Technology |
|---|-------------------------------------------------------------------------------------------|
| Frontend | React 19, TypeScript, Zustand 5, Leaflet, Tailwind CSS 3.4, Vite 8 (Rolldown) |
| Backend | NestJS 11 (Express 4 adapter), TypeScript, better-sqlite3, Zod (@trek/shared) |
| Real-time | WebSocket (ws) |
| Database | SQLite with WAL mode |
| Auth | JWT (HS256), bcrypt, TOTP MFA, OIDC |
| Maps | Leaflet + react-leaflet (default, CartoDB tiles), MapLibre GL, Mapbox GL, OSRM, Nominatim |
| i18n | 23 languages, EN canonical (locale directories live in shared/src/i18n/) |
Every translation key must exist in all 23 locales — the `i18n Key Parity` CI job fails on drift, so run `npm run i18n:parity:strict --workspace=shared` before pushing. Two directory names differ from the language they hold: Brazilian Portuguese is `br`, Greek is `gr`.
+4 -4
View File
@@ -4,7 +4,7 @@
## Opening the Dialog
Click the **New Trip** button in the dashboard toolbar (or the **Create First Trip** button on the empty state) to open the Create Trip dialog.
On desktop, click the floating **New Trip** button in the bottom-right corner of the dashboard, or the dashed **New Trip** tile at the end of the trip grid, to open the Create New Trip dialog. On mobile the floating button is replaced by the **+** in the centre of the bottom tab bar, and an empty dashboard also offers a **Create First Trip** button.
You can also open it directly via a deep link: navigate to `/dashboard?create=1`. This is the URL used by system notices that prompt you to create a trip.
@@ -28,11 +28,11 @@ You cannot set only one date and leave the other blank via normal interaction
### Currency
The trip's currency — its **accounting base**. Every expense in the Costs tab is converted into it, and every balance and settle-up suggestion is calculated in it. Defaults to **EUR**; 165 currencies are available.
The trip's currency — its **accounting base**. Every expense in the Costs tab is converted into it, and every balance and settle-up suggestion is calculated in it. The dialog pre-fills your own display currency (Settings → General → *Display currency*); if you left that on **Trip currency**, it falls back to **EUR**. An administrator can preset the instance-wide value under Admin → **User Defaults**, which new users inherit until they pick their own. 165 currencies are available.
Pick the currency you will actually settle up in. It is not a cosmetic label, but it is not a one-way door either: you can change it later from the same dialog (with the `trip_edit` permission), and TREK re-bases the existing expenses so no money moves — see [Currencies → Changing the trip currency](Currencies#changing-the-trip-currency).
> This is **not** the same as the display currency in Settings → General, which only changes what *you* read. See [Currencies](Currencies).
> This is **not** the same as the display currency in Settings → General, which changes what *you* read on trips that already exist. See [Currencies](Currencies).
### Cover Image
@@ -69,7 +69,7 @@ Add initial trip members from the members selector. On a **new** trip, selected
## Saving
Click **Create Trip**. The trip is saved and you are taken to the [Trip-Planner-Overview](Trip-Planner-Overview) for the new trip.
Click **Create New Trip**. The trip is saved, the dialog closes and a *"Trip created successfully!"* toast confirms it — you stay on the dashboard. The new trip is sorted into the trip list by its dates; click its card (or the Spotlight card, if it is the trip that earns that spot) to open the [Trip-Planner-Overview](Trip-Planner-Overview).
## Related Pages
+4 -2
View File
@@ -12,7 +12,7 @@ The short version: **the trip currency is the accounting base, the expense curre
## Trip currency
Every trip has exactly one currency. It is set when you create the trip and can be changed later in the trip edit dialog (requires the `trip_edit` permission). It defaults to **EUR**.
Every trip has exactly one currency. It is set when you create the trip and can be changed later in the trip edit dialog (requires the `trip_edit` permission). The new-trip dialog pre-fills it with **your display currency**, falling back to **EUR** when you have left that on *Trip currency*. This is the one moment the display currency touches stored data: it seeds the field, and from then on the trip's currency is its own.
This is the trip's **accounting base**. It is not a cosmetic label:
@@ -59,7 +59,9 @@ It has two modes:
| **Trip currency** (the default) | Each trip is shown in **its own** currency. A Tokyo trip reads in yen, a Moscow trip in roubles. |
| A specific currency (e.g. `USD`) | **Every** trip is converted into that currency for you, whatever its own currency is. |
Leave it on **Trip currency** unless you specifically want everything in your home currency regardless of where you are going. An administrator can set an instance-wide default for new users (Admin → Default User Settings); choosing **Trip currency** yourself overrides it, because it is a deliberate choice rather than an absence of one.
Leave it on **Trip currency** unless you specifically want everything in your home currency regardless of where you are going.
An administrator can set an instance-wide default (Admin → User Defaults), and it interacts with **Trip currency**: picking **Trip currency** stores an *empty* value, and an empty value on a defaultable setting counts as "not set", so the admin's currency is applied again (#1634). While an instance-wide default currency is configured, there is no way back to per-trip currencies from the Settings page — the admin has to clear it.
> Display conversion uses **live** rates, not the frozen ones — it is a view, and a view should reflect today. This is why a converted total can shift slightly day to day while the underlying balances stay rock steady.
+15 -13
View File
@@ -1,18 +1,24 @@
# Dashboard Widgets
The My Trips dashboard includes two utility widgets: a currency converter and a timezone clock.
The My Trips dashboard has a widget sidebar holding a currency converter, a timezone clock and a list of upcoming reservations, plus a Collections shortcut when the [Collections](Collections) addon is enabled (it ships disabled). Installed plugins can contribute further widgets to the same sidebar. This page covers the currency converter and the timezone clock.
![Dashboard Widgets](assets/DashboardWidgets.png)
## Where they appear
On large screens (desktop/wide tablet), both widgets appear in a sticky right-hand sidebar of the [My-Trips-Dashboard](My-Trips-Dashboard). On mobile and narrow screens, the widgets are accessible via **Quick Actions** buttons on the dashboard — tapping the Currency or Timezone button opens a bottom sheet containing the full widget.
On large screens (desktop/wide tablet), the widgets appear in a sticky right-hand sidebar of the [My-Trips-Dashboard](My-Trips-Dashboard). At 1280px and below that column stops being sticky and reflows underneath the trip grid as a wrapping row of cards. On a phone (under 768px) the mobile dashboard takes over and renders each enabled widget inline as its own panel, interleaved with the trip list in an order you can rearrange under Settings → Appearance. There is no bottom sheet for the widgets on any screen size.
Each user configures their own widgets independently. Whether each widget is shown or hidden is saved to your account on the server (synced across devices). The selected currency pair and saved timezone list are stored in your browser's local storage and are device-specific.
Each user configures their own widgets independently. Whether each widget is shown or hidden is saved to your account on the server (synced across devices). The selected currency pair and the saved timezone list live on your account as well, so they follow you to every device you sign in on.
### Showing and hiding widgets
On desktop, click the **Settings (gear) icon** in the dashboard toolbar to reveal toggle switches for each widget. Turning a widget off removes it from the sidebar; the preference is saved to your account.
Widget visibility lives in **Settings → Appearance**, under **Dashboard widgets** — there is no gear icon on the dashboard itself. Desktop and mobile are configured independently:
- **Desktop → Right sidebar** has a master switch for the whole column (turn it off and the dashboard centers) plus one switch each for Currency, Collections, Timezones and Upcoming reservations.
- **Desktop → Below the hero** switches the Atlas / countries, Trips total, Days traveled and Distance flown tiles.
- **Mobile → Bottom of page** switches Currency, Collections, Timezones and Upcoming reservations; **Mobile → Below the hero** switches Trips total and Days traveled.
Turning a widget off removes it from the dashboard; the preference is saved to your account.
---
@@ -31,7 +37,7 @@ 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.
**Supported currencies:** 162 currencies are available in the selector, including all major fiat currencies (USD, EUR, GBP, JPY, etc.) and many minor ones.
**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.
---
@@ -41,16 +47,12 @@ The timezone clock displays live clocks for multiple time zones simultaneously.
**How to use:**
- Your local time is always shown at the top.
- Below it, any zones you have added are listed with their current time and offset relative to your local zone.
- Click **+** to add a zone. You can pick from 18 preset city zones, or enter any IANA timezone identifier (e.g. `America/Denver`) with an optional custom label (if omitted, the city portion of the identifier is used as the label).
- Until you change the list, the widget starts with your local zone plus London and Tokyo. Your local zone is an ordinary row and can be removed like any other.
- Each row shows the zone's current time and its short timezone name or UTC offset (for example `GMT+9`), not an offset relative to your own zone.
- Click **+** to add a zone. A searchable dropdown lists every IANA timezone your browser knows (e.g. `America/Denver`); type to filter and pick one. The row is labelled with the city part of the identifier — there is no custom label.
- Hover over a zone row and click **×** to remove it.
**Preset zones (18):**
New York, London, Berlin, Paris, Dubai, Mumbai, Bangkok, Tokyo, Sydney, Los Angeles, Chicago, São Paulo, Istanbul, Singapore, Hong Kong, Seoul, Moscow, Cairo.
Clocks update every 10 seconds. The 12-hour or 24-hour format follows your display settings (see [Display-Settings](Display-Settings)).
Clocks refresh every 30 seconds and are always shown in 24-hour time — the 12-hour display setting does not apply to this widget.
---
+17 -8
View File
@@ -6,7 +6,7 @@ The Day Plan sidebar lets you organize places into days, add free-form notes, an
## The Day Plan sidebar
The Day Plan sidebar is the left panel in the trip planner. Each trip day is shown as a collapsible section. Expanded or collapsed state is saved per trip in `sessionStorage` (key: `day-expanded-{tripId}`), so your layout is preserved across page reloads in the same browser session.
The Day Plan sidebar is the left panel in the trip planner. Each trip day is shown as a collapsible section. Expanded or collapsed state is saved per trip in `localStorage` (key: `day-expanded-{tripId}`), so your layout is preserved across page reloads and between browser sessions on that device.
## Day timeline
@@ -32,7 +32,7 @@ Items are sorted by their time or position index.
You can also reorder places within a day, or move them to a different day, by dragging and dropping inside the sidebar.
To remove a place from a day, click the **X** button next to the place in the day timeline.
To remove a place from a day, right-click the entry in the day timeline and choose **Remove from day**, or select the place and use the **Remove from Day** button in the place detail panel. On mobile, switch the plan screen to **Plan** and tap the **X** next to the place. Deleting the place itself, from the same right-click menu, removes it from every day.
![Removing a place by button](assets/DayItineraryRemovePlaceByButton.gif)
@@ -44,17 +44,19 @@ A reservation that spans multiple days appears in each relevant day with a phase
|---|---|---|---|
| Flight | Departure | In transit | Arrival |
| Car | Pickup | Active | Return |
| Parking | Drop-off | Not shown | Pickup |
| Other | Start | Ongoing | End |
Car rentals that are in the "Active" (middle) phase are shown in the day header rather than the timeline.
Car rentals that are in the "Active" (middle) phase are shown in the day header rather than the timeline. A multi-day parking booking is dropped from the days in between entirely — it appears on its drop-off day and its pickup day only, with no day header badge.
## Day notes
Click the note **+** button in any day section to add a note. Notes have three fields:
Click the note **+** button in any day section to add a note. Notes have four fields:
- **Title** (required) — the main note text shown in the timeline
- **Subtitle / detail** (optional) — a free-form text field (Markdown supported) displayed beneath the title
- **Icon** — choose from 20 icons: FileText, Info, Clock, MapPin, Navigation, Train, Plane, Bus, Car, Ship, Coffee, Ticket, Star, Heart, Camera, Flag, Lightbulb, AlertTriangle, ShoppingBag, Bookmark
- **Icon** — choose from 32 icons: FileText, Info, Clock, MapPin, Navigation, Train, Plane, Bus, Car, Ship, Coffee, Ticket, Star, Heart, Camera, Flag, Lightbulb, AlertTriangle, ShoppingBag, Bookmark, Utensils, Wine, ParkingSquare, Fuel, Footprints, Mountain, Waves, Sun, Umbrella, Music, Landmark, Gift
- **Colour** (optional) — tints the note card in the day timeline; the dialog shows a live preview of the card the note will become
Notes interleave with places and transports in the day timeline and are ordered by their `sort_order`. Use the **↑ / ↓** chevron buttons on a note to reposition it within the merged timeline. Notes can also be repositioned by dragging.
@@ -72,11 +74,18 @@ The panel can be collapsed to a slim header bar or closed entirely with the **X*
At the top of the Day Plan sidebar:
- **Export PDF** — downloads a PDF of the full trip plan. See [PDF-Export](PDF-Export).
- **ICS** — exports the trip as a calendar file (.ics) for import into calendar apps.
- **Export** — opens the export dialog, which holds every format in three groups: **Document** (a PDF of the full trip plan, see [PDF-Export](PDF-Export)), **Calendar** (a `.ics` file for import into calendar apps, plus a subscribable calendar feed for members who can manage share links) and **Maps & GPS** (GPX — whole trip, places only, or days as routes).
- **Expand / Collapse all** — toggles all day sections open or closed at once.
- **Undo** — reverses the last drag, reorder, or assign action.
- **Reorder days** — reorder the days of the trip or insert a new one. A day's places, notes and bookings move with it. Shown to members who can edit days.
- **Show all booking routes** — draws the connection of every routable booking on the map at once. Shown once the trip has at least one routable booking.
Route controls (optimize order, open in Google Maps) appear inside each expanded day section after the place list.
Route controls appear at the bottom of a day section, after the place list, and only for the day you have selected — click a day header to select it. The row only appears on a day that can actually be routed: two or more places on the day, a single located place that accommodation optimization can bookend with a hotel, or a transfer day where you check out of one hotel and into another. On a phone the same controls sit in the **Daily Overview** sheet instead, opened from the pill above the plan timeline, and appear there when the day has two located places, or one located place plus a hotel to start from.
- **Route** — draws that day's route on the map.
- **Open in Google Maps** — hands the day's stops to Google Maps as a route, in planned order.
- **Open in CoMaps** — the same day handed to CoMaps for offline navigation, carrying the day's travel mode.
- **Optimize** — reorders the day's places into the shortest route. See [Route-Optimization](Route-Optimization).
- **Travel mode** — Driving or Walking for that day, plus any travel mode a plugin adds.
**See also:** [Places-and-Search](Places-and-Search) · [Map-Features](Map-Features) · [Route-Optimization](Route-Optimization) · [Weather-Forecasts](Weather-Forecasts) · [Reservations-and-Bookings](Reservations-and-Bookings)
+6 -2
View File
@@ -38,15 +38,19 @@ The admin account is unaffected and retains full access.
TREK schedules an automatic hourly reset of the demo database. At each reset:
1. The current `travel.db` is replaced with the saved baseline (`travel-baseline.db`).
2. The admin account's credentials (`password_hash`, API keys, avatar) are re-applied on top of the restored baseline, so admin API keys and password changes survive the reset.
2. The admin account's credentials (`password_hash`, API keys, avatar) are re-applied on top of the restored baseline, so admin API keys and password changes survive the reset — but only when `DEMO_ADMIN_EMAIL` is set explicitly.
If no baseline has been saved yet, the reset is skipped and a message is logged.
The seeder defaults the admin address to `admin@trek.app` while the reset defaults it to `admin@nomad.app`, a legacy quirk that is pinned deliberately. With `DEMO_ADMIN_EMAIL` unset the reset looks up an address that does not exist, finds no admin row, and skips the carry-over entirely — a password change, API key or avatar set after the baseline was saved is lost on every hourly reset. Set `DEMO_ADMIN_EMAIL` (to `admin@trek.app`, for instance) if you want them to survive. Unlike the seed-time variables above, the reset reads it on every run, so setting it on an already-seeded instance and restarting is enough.
The instance-wide Maps and Unsplash keys stored in `app_settings` are carried across either way, so map and photo search keep working after a reset.
## Saving a baseline
The baseline is the snapshot the hourly reset restores to. The admin can update it at any time:
**Endpoint:** `POST /admin/save-demo-baseline`
**Endpoint:** `POST /api/admin/save-demo-baseline`
This is available in the admin panel. The baseline captures the current state of the database — including trip data, settings, and encrypted API keys — so demo features (maps, photos, weather) continue to work after each reset.
+38 -23
View File
@@ -71,12 +71,18 @@ Branch naming conventions:
## 5. Install Dependencies
The repo is an npm workspace monorepo. One command at the root installs everything:
The repo is an npm workspace monorepo with three workspaces — `shared`, `server`, and `client`. One command at the root installs all three:
```bash
npm ci
```
`plugin-sdk/` is **not** a root workspace: it has its own lockfile and is published to npm independently, so a root `npm ci` never touches it. If you are working on the SDK, install and run its commands from that directory:
```bash
cd plugin-sdk && npm ci
```
---
## 6. Optional: KItinerary (Booking Import)
@@ -94,17 +100,17 @@ sudo apt-get install -y libkitinerary-bin
Add these to your local `.env` (or export them before starting the server):
```bash
# Required: path to the extractor binary
KITINERARY_EXTRACTOR_PATH=/usr/local/bin/kitinerary-extractor
# Prevent Qt from probing for a display in headless/server environments
QT_QPA_PLATFORM=offscreen
# KDE cache directory (avoids writing to $HOME)
XDG_CACHE_HOME=/tmp/kf6-cache
# Optional: only needed when the binary is not found on its own
# KITINERARY_EXTRACTOR_PATH=/usr/lib/x86_64-linux-gnu/libexec/kf6/kitinerary-extractor
```
You can override `KITINERARY_EXTRACTOR_PATH` if you installed the binary to a different location.
`KITINERARY_EXTRACTOR_PATH` is optional. Left unset, the server looks for the Debian/Ubuntu location `/usr/lib/<triplet>/libexec/kf6/kitinerary-extractor` and then for `kitinerary-extractor` on `PATH` — one of which is what `libkitinerary-bin` gives you. Set it only if the binary ended up somewhere neither lookup reaches, and make sure the path exists: a value that does not resolve logs a warning and stops the lookup instead of falling back, which turns the import feature off. The Docker image sets it to `/usr/local/bin/kitinerary-extractor`, a symlink created at image build; that path does not exist on a plain apt install.
---
@@ -119,7 +125,7 @@ These commands run across all workspaces at once and are the recommended way to
| `npm run dev` | Build shared, then start shared (watch), server, and client together via `concurrently` |
| `npm run build` | Build shared → server → client in order |
| `npm test` | Run tests in shared, server, and client |
| `npm run test:cov` | Run coverage for server and client |
| `npm run test:cov` | Run coverage for shared, server, client and plugin-sdk |
| `npm run test:e2e` | Run end-to-end tests (server) |
| `npm run lint` | Lint shared, server, and client |
| `npm run format` | Format shared, server, and client |
@@ -133,7 +139,7 @@ The `@trek/shared` package is the single source of truth for code shared between
| Command | Description |
|-----------------------------|--------------------------------------|
| `npm run build` | Compile shared package (tsup) |
| `npm run build` | Compile shared package (tsdown) |
| `npm run build:watch` | Compile in watch mode |
| `npm test` | Run tests |
| `npm run test:watch` | Run tests in watch mode |
@@ -146,21 +152,26 @@ The `@trek/shared` package is the single source of truth for code shared between
### Server (`/server`)
| Command | Description |
|----------------------------|------------------------------------------|
| `npm start` | Start the server (production) |
| `npm run dev` | Start the server in watch mode |
| `npm run build` | Compile server |
| `npm run typecheck` | Type-check without emitting |
| `npm test` | Run all tests |
| `npm run test:unit` | Run unit tests only |
| `npm run test:integration` | Run integration tests |
| `npm run test:ws` | Run WebSocket tests |
| `npm run test:e2e` | Run end-to-end tests |
| `npm run test:watch` | Run tests in watch mode |
| `npm run test:coverage` | Run tests with coverage report |
| `npm run lint` | Lint source |
| `npm run format` | Format source |
> **Tip:** `tests/` sits outside the build `tsconfig.json`, so `npm run typecheck` skips it — `npm run typecheck:tests` is the only step that catches a broken test call site. CI runs both.
| Command | Description |
|------------------------------|------------------------------------------|
| `npm start` | Start the server (production) |
| `npm run dev` | Start the server in watch mode |
| `npm run build` | Compile server |
| `npm run typecheck` | Type-check without emitting |
| `npm run typecheck:tests` | Type-check `tests/` too (CI gate) |
| `npm test` | Run all tests |
| `npm run test:unit` | Run unit tests only |
| `npm run test:integration` | Run integration tests |
| `npm run test:ws` | Run WebSocket tests |
| `npm run test:e2e` | Run end-to-end tests |
| `npm run test:watch` | Run tests in watch mode |
| `npm run test:coverage` | Run tests with coverage report |
| `npm run lint` | Lint source |
| `npm run lint:check` | Lint everything, no `--fix` (CI gate) |
| `npm run check:plugin-facts` | Verify generated plugin facts (CI gate) |
| `npm run format` | Format source |
### Client (`/client`)
@@ -169,12 +180,16 @@ The `@trek/shared` package is the single source of truth for code shared between
| `npm run dev` | Start the Vite dev server |
| `npm run build` | Build for production (runs icon generation first) |
| `npm run preview` | Preview the production build locally |
| `npm run typecheck` | Type-check without emitting (CI gate) |
| `npm test` | Run all tests |
| `npm run test:unit` | Run unit tests only |
| `npm run test:integration` | Run integration tests |
| `npm run test:watch` | Run tests in watch mode |
| `npm run test:coverage` | Run tests with coverage report |
| `npm run lint` | Lint source |
| `npm run lint:check` | Same command as `npm run lint` — the name CI uses |
| `npm run lint:pages` | Enforce the Page pattern (CI gate) |
| `npm run theme:lint` | Flag styling that bypasses the appearance tokens |
| `npm run format` | Format source |
---
@@ -199,5 +214,5 @@ Then open a Pull Request from your fork to `liketrek/TREK` targeting the `dev` b
## Tips
- Always branch off from an up-to-date `dev` — run `git fetch upstream && git rebase upstream/dev` before starting new work.
- Run tests before pushing: `npm test` at the repo root runs all workspaces.
- Run tests before pushing: `npm test` at the repo root runs all workspaces. That alone is not the full CI gate — also run `npm run typecheck && npm run typecheck:tests && npm run lint:check && npm run check:plugin-facts` in `server/`, `npm run typecheck && npm run lint:check && npm run lint:pages` in `client/`, and `npm run i18n:parity:strict --workspace=shared` at the root if you touched translations.
- Follow the commit message conventions described in the [[Contributing]] guidelines.
+8 -2
View File
@@ -8,7 +8,7 @@ The General tab (Settings → General) controls your locale preferences and a fe
Open the user menu in the top navigation bar, select **Settings**, and stay on the **General** tab — it is the tab the page opens on.
The tab is split into three sections: **Startup** (where opening TREK lands), **Language & region** (currency, language, temperature, distance, time format) and **Travel & map** (booking route labels, POI pills, blur booking codes).
The tab is split into three sections: **Startup** (where opening TREK lands), **Language & region** (currency, language, temperature, distance, time format) and **Travel & map** (booking route labels, always show booking routes, explore places on the map, blur booking codes, optimize route from accommodation).
> Color mode (Light / Dark / Auto) is **not** here — it lives on the **Appearance** tab. See [Appearance-Settings](Appearance-Settings).
@@ -44,7 +44,7 @@ The parameter takes the planner's internal tab ids, which are historic German na
| Tab | Id |
|-----|-----|
| Plan | `plan` |
| Transport | `transports` |
| Transports | `transports` |
| Bookings | `buchungen` |
| Lists | `listen` |
| Costs | `finanzplan` |
@@ -116,6 +116,12 @@ This only sets the *default* for a trip you haven't touched before. If you've al
When enabled, confirmation codes and reference numbers are blurred until you hover or tap. Set to **On** or **Off**.
## Optimize route from accommodation
When **On** (the default), the **Optimize** button in the day's route tools anchors the route on that day's accommodation instead of only reordering the places among themselves: an ordinary day becomes a loop out from the hotel and back to it, a transfer day a run from the hotel you check out of to the one you check into that evening. Set to **On** or **Off**.
A day without an accommodation, or one whose hotel has no coordinates, is optimized from its first place as before. Locked and timed places keep their slots either way. See [Route-Optimization](Route-Optimization).
## See also
- [Currencies](Currencies)
+3 -3
View File
@@ -17,14 +17,14 @@ Open the **Files** tab inside the trip planner, or navigate directly to `/trips/
Drag and drop files onto the upload area, click it to open the file picker, or paste an image directly into the Files panel.
- **Maximum file size:** 50 MB per file.
- **Blocked file types:** `.svg`, `.html`, `.htm`, `.xml` — these are always rejected.
- **Default allowed types:** jpg, jpeg, png, gif, webp, heic, pdf, doc, docx, xls, xlsx, txt, csv, pkpass. An admin can customize the allowed list in [Admin-Addons](Admin-Addons).
- **Blocked file types:** renderable documents (`.svg`, `.svgz`, `.html`, `.htm`, `.shtml`, `.shtm`, `.xml`, `.xhtml`, `.xht`), scripts (`.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs`, `.php`, `.py`, `.rb`, `.pl`) and executables (`.exe`, `.bat`, `.sh`, `.cmd`, `.msi`, `.dll`, `.com`, `.vbs`, `.ps1`, `.app`) — these are always rejected, as is any file whose MIME type contains `svg`.
- **Default allowed types:** jpg, jpeg, png, gif, webp, heic, pdf, doc, docx, xls, xlsx, txt, csv, pkpass, pkpasses, md, markdown. An admin can customize the allowed list under **Admin → Settings → Allowed File Types** — comma-separated extensions, or `*` to allow everything except the blocked types above.
Requires the `file_upload` permission.
## Browsing and filtering
The toolbar provides filter tabs: **All**, **PDF**, **Images**, **Docs**, and conditionally **Starred** (only shown when at least one file is starred) and **Collab** (only shown when files exist from collaborative notes). Each tab shows a count badge.
The toolbar provides filter tabs, in this order: **All**, a star icon for **Starred** (only shown when at least one file is starred), **PDFs**, **Images**, **Documents**, and **Collab Notes** (only shown when files exist from collaborative notes). Each tab shows a count badge.
## Previewing files
+12 -5
View File
@@ -9,14 +9,17 @@ TREK encrypts sensitive settings at rest using AES-256-GCM. The following values
- Mapbox access token (per user)
- OpenWeather API key (per user)
- Immich API key (per user)
- AirTrail API key (per user)
- Synology Photos password, session ID, and device ID (per user)
- Per-user webhook URL and ntfy notification token (in `settings` table)
- Per-user webhook URL, ntfy notification token, and LLM provider API key (in `settings` table)
- OIDC client secret (global, in `app_settings`)
- SMTP password (global, in `app_settings`)
- Admin webhook URL and admin ntfy token (global, in `app_settings`)
- MFA (TOTP) secrets for all users
- Photo passphrases for Synology shared-link photos (in `trek_photos`)
- Passphrases for shared trip album links (in `trip_album_links`)
- S3 storage backend secret access keys (in `app_settings['storage.backends']`; see [Admin-Storage](Admin-Storage))
- Plugin OAuth access and refresh tokens, plus every plugin settings field the plugin's manifest marks `secret` (instance scope in `plugins.config`, user scope in `plugin_user_config.config`)
The encryption derives a key from `ENCRYPTION_KEY` using SHA-256 (with a domain suffix per secret type), so the raw `ENCRYPTION_KEY` value is never stored in the database.
@@ -35,7 +38,9 @@ All encrypted settings (API keys, SMTP password, OIDC secret, MFA secrets, notif
## Backing up the key
Your backup ZIP does **not** include the encryption key. Store the key separately from your backups — for example, in a password manager or a secrets manager. See [Backups](Backups).
Unless you set `ENCRYPTION_KEY` in the environment, your backup ZIP **does** include the encryption key: `./data/.encryption_key` is bundled into every archive, manual and automatic, as a root-level `.encryption_key` entry, so a restore onto a different install can still decrypt the stored secrets. That makes the ZIP exactly as sensitive as the key itself — store and transfer it accordingly.
On installs that supply the key through the environment the file is not the source of truth, so no backup carries it. Keep it somewhere separate — for example, in a password manager or a secrets manager. See [Backups](Backups).
To find your current key: check the `ENCRYPTION_KEY` environment variable or read `./data/.encryption_key`.
@@ -46,7 +51,7 @@ Use `scripts/migrate-encryption.ts` to re-encrypt all stored secrets without dow
**Docker:**
```bash
docker exec -it trek node --import tsx scripts/migrate-encryption.ts
docker exec -it -w /app/server trek node --import tsx scripts/migrate-encryption.ts
```
**Host (run from the `server/` directory):**
@@ -63,8 +68,10 @@ The script:
4. Re-encrypts all stored secrets across all tables:
- `app_settings`: `oidc_client_secret`, `smtp_pass`, `admin_webhook_url`, `admin_ntfy_token`, `maps_api_key`, `unsplash_api_key`
- `app_settings['storage.backends']`: the `secretAccessKey` of every S3 storage backend
- `users` (per user): `maps_api_key`, `unsplash_api_key`, `openweather_api_key`, `immich_api_key`, `synology_password`, `synology_sid`, `synology_did`, `mfa_secret`
- `settings` (per user): `webhook_url`, `ntfy_token`, `mapbox_access_token`
- `users` (per user): `maps_api_key`, `unsplash_api_key`, `openweather_api_key`, `immich_api_key`, `synology_password`, `synology_sid`, `synology_did`, `airtrail_api_key`, `mfa_secret`
- `settings` (per user): `webhook_url`, `ntfy_token`, `mapbox_access_token`, `llm_api_key`
- `plugin_oauth_tokens`: `access_token`, `refresh_token` (per plugin and user)
- `plugins.config` and `plugin_user_config.config`: every settings field a plugin's manifest marks `secret`, resolved per plugin and scope from `plugin_settings_fields`
- `trip_album_links`: `passphrase`
- `trek_photos`: `passphrase`
5. Reports counts of migrated, already-migrated, skipped (empty), and errored values.
+56 -24
View File
@@ -6,12 +6,39 @@ Complete reference for all environment variables TREK reads.
- **Docker Compose** — use the `environment:` block or a `.env` file alongside `docker-compose.yml`
- **Docker run** — pass each variable with `-e VARIABLE=value`
- **Helm** — use `env:` for plain values and `secretEnv:` for sensitive values in `values.yaml`
- **Helm** — use `env:` for plain values and `secretEnv:` for sensitive values in `values.yaml`. The chart only
passes through the keys it declares (26 in `templates/configmap.yaml`, 5 in `templates/secret.yaml`), so a variable
that is not one of them is dropped silently — patch it onto the Deployment or add it to the chart
- **Unraid** — set in the container template editor
- **Proxmox Community Script** — set in `/opt/trek/server/.env`
---
## Startup Validation
TREK checks almost all of this surface once at boot. A variable that is **unset or blank** falls back to its documented
default; a variable that is **present but malformed** aborts startup, with an aggregated report naming every
offending value:
```
Invalid environment configuration:
- PORT="not-a-port": must be a port number (1-65535)
- SESSION_DURATION="bogus": must be a duration like "1h", "7d" or "30d"
```
In Docker this crash-loops the container until the value is corrected or removed. Boolean switches accept
`true`/`false`, `1`/`0`, `on`/`off`, `yes`/`no` in any casing — anything else counts as malformed. Variables TREK
does not know are passed through untouched.
`TREK_DB_JOURNAL_MODE` and `TREK_DB_SYNCHRONOUS` are exceptions: they log a warning and fall back instead of
aborting, because `reset-admin.js` — the way back into a locked-out instance — reads the same two variables and has
to keep working. The three plugin caps `TREK_PLUGIN_AI_PER_DAY`, `TREK_PLUGIN_NOTIFY_PER_DAY` and
`TREK_PLUGIN_AUDIT_MAX_ROWS` are read by the plugin host rather than by the boot schema, so a malformed value there
falls back to the default without a warning. `NODE_ENV` and `TZ` are not validated at all, so a non-standard value
like `NODE_ENV=staging` still boots.
---
## Core
| Variable | Description | Default |
@@ -23,8 +50,8 @@ Complete reference for all environment variables TREK reads.
| `TZ` | Timezone for logs, reminders, and cron jobs (e.g. `Europe/Berlin`) | `UTC` |
| `LOG_LEVEL` | `info` = concise user actions; `debug` = verbose details | `info` |
| `DEFAULT_LANGUAGE` | Default language on the login page — see supported codes below | `en` |
| `SESSION_DURATION` | How long a login session stays valid before re-login is required. Used when **"Remember me" is unchecked** on the login form (the default): applies to the `trek_session` JWT `exp` claim, and the cookie is issued as a **browser-session cookie** (no `maxAge`, cleared when the browser closes). Accepts `ms`-style strings: `1h`, `12h`, `7d`, `30d`, `90d`. Invalid values warn at startup and fall back to the default. Does not affect the short-lived MFA challenge token or MCP OAuth tokens (those keep their own TTL). | `24h` |
| `SESSION_DURATION_REMEMBER` | Session length used when the user **ticks "Remember me"** on login: a longer-lived JWT `exp` claim plus a **persistent** `trek_session` cookie whose `maxAge` matches, so the session survives browser restarts. Same `ms`-style format and startup-fallback behaviour as `SESSION_DURATION`. | `30d` |
| `SESSION_DURATION` | How long a login session stays valid before re-login is required. Used when **"Remember me" is unchecked** on the login form (the default): applies to the `trek_session` JWT `exp` claim, and the cookie is issued as a **browser-session cookie** (no `maxAge`, cleared when the browser closes). Accepts `ms`-style strings: `1h`, `12h`, `7d`, `30d`, `90d`. Invalid values abort startup — see Startup Validation above. Does not affect the short-lived MFA challenge token or MCP OAuth tokens (those keep their own TTL). | `24h` |
| `SESSION_DURATION_REMEMBER` | Session length used when the user **ticks "Remember me"** on login: a longer-lived JWT `exp` claim plus a **persistent** `trek_session` cookie whose `maxAge` matches, so the session survives browser restarts. Same `ms`-style format and the same startup validation as `SESSION_DURATION`. | `30d` |
| `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). | — |
@@ -65,7 +92,7 @@ Setting `ENCRYPTION_KEY` explicitly is recommended so you can back it up indepen
### `DEFAULT_LANGUAGE` — Supported Codes
You can set `DEFAULT_LANGUAGE` to any of the 22 languages TREK ships. The currently supported codes are:
You can set `DEFAULT_LANGUAGE` to any of the 23 languages TREK ships. The currently supported codes are:
| Code | Language |
|---------|--------------------|
@@ -91,8 +118,10 @@ You can set `DEFAULT_LANGUAGE` to any of the 22 languages TREK ships. The curren
| `gr` | Ελληνικά |
| `sv` | Svenska |
| `vi` | Tiếng Việt |
| `ca` | Català |
If you set a code that isn't supported, TREK falls back to English (`en`). This list grows as new
If you set a code that isn't on this list, TREK refuses to start and prints
`DEFAULT_LANGUAGE="…": must be one of: …`. Leave the variable unset to use English (`en`). This list grows as new
translations are added to TREK.
---
@@ -130,7 +159,7 @@ full explanation.
| `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` |
| `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) |
| `COOKIE_SECURE` | Controls the `secure` flag on the `trek_session` cookie. Auto-derived as `true` when `NODE_ENV=production` or `FORCE_HTTPS=true`. Set to `false` only as an escape hatch for LAN testing without TLS — not recommended in production. | auto |
| `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.
@@ -147,7 +176,7 @@ For setup instructions, see [OIDC-SSO](OIDC-SSO).
| `OIDC_CLIENT_SECRET` | OIDC client secret | — |
| `OIDC_DISPLAY_NAME` | Label shown on the SSO login button | `SSO` |
| `OIDC_ONLY` | Force SSO-only mode: disables password login and registration, overrides Admin > Settings toggles, cannot be changed at runtime. First SSO login becomes admin on a fresh instance. | `false` |
| `OIDC_ADMIN_CLAIM` | OIDC claim used to identify admin users (e.g. `groups`) | — |
| `OIDC_ADMIN_CLAIM` | OIDC claim inspected for the admin role. Only takes effect once `OIDC_ADMIN_VALUE` is set. | `groups` |
| `OIDC_ADMIN_VALUE` | Value of the OIDC claim that grants admin role (e.g. `app-trek-admins`) | — |
| `OIDC_SCOPE` | Space-separated OIDC scopes to request. **Fully replaces** the default — always include `openid email profile` plus any extra scopes (e.g. add `groups` when using `OIDC_ADMIN_CLAIM`) | `openid email profile` |
| `OIDC_DISCOVERY_URL` | Override the auto-constructed OIDC discovery endpoint. Required for providers with a non-standard path (e.g. Authentik) | — |
@@ -207,7 +236,7 @@ For setup instructions, see [MCP-Overview](MCP-Overview).
| Variable | Description | Default |
|----------------------------|---------------------------------------------------------------------------------------------------------|---------|
| `MCP_RATE_LIMIT` | Max MCP API requests per user per minute | `300` |
| `MCP_RATE_LIMIT` | Max MCP API requests per minute, counted **per user and per OAuth client** — a user connected from two MCP clients gets this budget twice. Requests made with a static `trek_` token or a session JWT carry no client and share one further bucket per user. | `300` |
| `MCP_MAX_SESSION_PER_USER` | Max concurrent MCP sessions per user. At the cap the user's least-recently-active session is closed to make room — requests are not rejected. | `20` |
| `MCP_SESSION_TTL` | Session idle timeout in seconds (max 86400) | `3600` |
| `MCP_SSE_KEEPALIVE` | SSE keep-alive ping interval in seconds — keeps the stream alive through reverse proxies. `0` disables the pings; an open stream still refreshes the session's idle timeout. | `25` |
@@ -232,11 +261,13 @@ Request bodies validated with Zod are documented automatically from the same sch
|-----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|
| `KITINERARY_EXTRACTOR_PATH` | Full path to the `kitinerary-extractor` binary. When unset, TREK searches `/usr/lib/*/libexec/kf6/kitinerary-extractor` and then `PATH`. Set this if you install the binary to a non-standard location. | auto-detected |
The official TREK Docker image bundles the binary automatically: on amd64 it downloads the static release from
`https://cdn.kde.org/ci-builds/pim/kitinerary/`; on arm64 it installs `libkitinerary-bin` via apt (Debian trixie). When
running TREK from source, install `libkitinerary-bin` (Debian trixie / Ubuntu 25.04+) or download the static binary
directly and place it anywhere on `PATH`. The `GET /api/health/features` endpoint returns `{ "bookingImport": true }`
when the binary is found, and the Import button in the Reservations panel is hidden when it is not.
The official TREK Docker image bundles the binary automatically: on both amd64 and arm64 it installs
`libkitinerary-bin` via apt (Debian trixie) and symlinks it to `/usr/local/bin/kitinerary-extractor`, which the image
also pins via `KITINERARY_EXTRACTOR_PATH`. When running TREK from source, install `libkitinerary-bin` (Debian trixie /
Ubuntu 25.04+) or download the static binary directly and place it anywhere on `PATH`. The
`GET /api/health/features` endpoint returns `{ "bookingImport": true }` when the binary is found. The Import button in
the Reservations panel is hidden only when **neither** extractor is available — an instance running the AI Parsing
addon still offers it with `bookingImport: false`.
Booking import can also fall back to an AI model for documents KDE Itinerary can't read. That feature (the **AI Parsing** addon) is configured entirely in the UI and needs no environment variables — see [AI-Booking-Import](AI-Booking-Import).
@@ -279,12 +310,13 @@ Storage backends and category assignment are configured in
[[Admin: Storage|Admin-Storage]] (or a seed-once `storage-config.json`), not
through environment variables. `TREK_PLACE_PHOTO_DIR` below is unaffected.
| Variable | Description | Default |
|--------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------|
| `TREK_PLACE_PHOTO_DIR` | Directory where cached Google place photos are stored. Created recursively on boot. Set this to point photo storage at a dedicated mounted volume. | `uploads/photos/google` |
| `BACKUP_UPLOAD_LIMIT_MB` | Maximum **compressed** size (in MB) of a restore-backup archive that may be uploaded. Raise it if your backups (which include the `uploads/` directory) exceed the default. Non-positive or invalid values log a warning and fall back to the default. | `500` |
| `TREK_DB_JOURNAL_MODE` | SQLite [journal mode](https://sqlite.org/pragma.html#pragma_journal_mode): `DELETE`, `TRUNCATE`, `PERSIST`, `MEMORY`, `WAL` or `OFF`. Set `DELETE` when the data directory lives on network storage — see below. Values SQLite doesn't know log a warning and fall back. | `WAL` |
| `TREK_DB_SYNCHRONOUS` | SQLite [synchronous](https://sqlite.org/pragma.html#pragma_synchronous) level: `OFF`, `NORMAL`, `FULL` or `EXTRA`. The default follows the journal mode — `NORMAL` under WAL (what SQLite itself uses there), `FULL` otherwise, because a rollback journal at `NORMAL` can lose committed transactions on a power cut. | `NORMAL` / `FULL` |
| Variable | Description | Default |
|------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------|
| `TREK_PLACE_PHOTO_DIR` | Directory where cached Google place photos are stored. Created recursively on boot. Set this to point photo storage at a dedicated mounted volume. | `uploads/photos/google` |
| `BACKUP_UPLOAD_LIMIT_MB` | Maximum **compressed** size (in MB) of a restore-backup archive that may be uploaded. Raise it if your backups (which include the `uploads/` directory) exceed the default. Non-positive or invalid values abort startup. | `500` |
| `BACKUP_MAX_DECOMPRESSED_MB` | Maximum **decompressed** size (in MB) of a restore-backup archive — the zip-bomb guard. Independent of `BACKUP_UPLOAD_LIMIT_MB` and enforced on both restore paths, so a restore that fits the upload cap can still be refused with `Backup exceeds the maximum decompressed size.` Raise both when restoring a very large instance. | `5120` (5 GB) |
| `TREK_DB_JOURNAL_MODE` | SQLite [journal mode](https://sqlite.org/pragma.html#pragma_journal_mode): `DELETE`, `TRUNCATE`, `PERSIST`, `MEMORY`, `WAL` or `OFF`. Set `DELETE` when the data directory lives on network storage — see below. Values SQLite doesn't know log a warning and fall back. | `WAL` |
| `TREK_DB_SYNCHRONOUS` | SQLite [synchronous](https://sqlite.org/pragma.html#pragma_synchronous) level: `OFF`, `NORMAL`, `FULL` or `EXTRA`. The default follows the journal mode — `NORMAL` under WAL (what SQLite itself uses there), `FULL` otherwise, because a rollback journal at `NORMAL` can lose committed transactions on a power cut. | `NORMAL` / `FULL` |
### Running the database on network storage
@@ -314,9 +346,9 @@ next key rotation or admin reset would quietly switch the file back to WAL.
| Variable | Description | Default |
|---------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------|
| `IDEMPOTENCY_TTL_SECONDS` | How long (in seconds) stored idempotency keys are kept before garbage collection. The offline client replays queued mutations with their `X-Idempotency-Key` on reconnect, so this must exceed the longest expected offline window or a replay could create a duplicate. Invalid values silently fall back to the default. | `2592000` (30 days) |
| `IDEMPOTENCY_TTL_SECONDS` | How long (in seconds) stored idempotency keys are kept before garbage collection. The offline client replays queued mutations with their `X-Idempotency-Key` on reconnect, so this must exceed the longest expected offline window or a replay could create a duplicate. Invalid values abort startup. | `2592000` (30 days) |
| `OVERPASS_URL` | Custom [Overpass API](https://wiki.openstreetmap.org/wiki/Overpass_API) endpoint(s) used by the map's POI "explore" search, comma-separated. When set it **replaces** the bundled public mirrors — point it at an internal or self-hosted Overpass instance when the public mirrors are unreachable from your network (e.g. firewalled/locked-down egress in a Kubernetes cluster). Entries that aren't valid `http(s)` URLs are ignored. If you don't run your own Overpass but the public mirrors throttle TREK, first make sure `APP_URL` (or `ALLOWED_ORIGINS`) is set: that alone gives outbound Overpass/Nominatim requests a unique User-Agent, which the public mirrors rate-limit far less. | bundled public mirrors |
| `OVERPASS_TIMEOUT_MS` | Per-endpoint timeout (in milliseconds) for Overpass POI requests. Endpoints race in parallel and one that hasn't answered within this window is abandoned so a faster mirror can win. Raise it if you run a slow self-hosted Overpass instance. Invalid values fall back to the default. | `12000` |
| `OVERPASS_TIMEOUT_MS` | Per-endpoint timeout (in milliseconds) for Overpass POI requests. Endpoints race in parallel and one that hasn't answered within this window is abandoned so a faster mirror can win. Raise it if you run a slow self-hosted Overpass instance. Invalid values abort startup. | `12000` |
---
@@ -347,15 +379,15 @@ The plugin system is **on by default**. The runtime and the Admin → Plugins pa
| `TREK_PLUGINS_DATA_DIR` | Directory for each plugin's own **data** (its private SQLite file). Kept separate from the code tree; persist it as a volume too. | `<data>/plugins-data` |
| `TREK_PLUGIN_REGISTRY_URL` | Override the plugin registry index the *Discover* tab browses. Point it at your own fork or mirror of the registry. | `https://raw.githubusercontent.com/liketrek/TREK-Plugins/main/dist/index.json` |
| `TREK_PLUGIN_MAX_RSS_MB` | Per-plugin memory ceiling in MB. A plugin process that exceeds it is stopped. | `300` |
| `TREK_PLUGIN_PERMISSIONS` | Set to `off` to opt **out** of the Node.js OS-level permission sandbox for plugin child processes (not recommended). Any other value keeps the sandbox on. | `on` |
| `TREK_PLUGIN_PERMISSIONS` | Set to any falsy value (`off`, `false`, `0`, `no` — any casing) to opt **out** of the Node.js OS-level permission sandbox for plugin child processes (not recommended). A truthy value, or leaving it unset, keeps the sandbox on; anything outside that set is rejected at boot. | `on` |
| `TREK_PLUGIN_ALLOW_PRIVATE_EGRESS`| Set to `on` to let a plugin's declared outbound hosts resolve to private/internal addresses (e.g. a service on your LAN). By default connections to private, loopback, link-local and metadata addresses are refused. | off (private egress blocked) |
| `TREK_PLUGINS_DEV_LINK` | **Development only.** Set to exactly `1` to enable *dev-link*: registering a plugin from a local build directory and hot-reloading it against a live instance's data. Dev-linked code bypasses the install-time signature/integrity checks and (under `npm run dev`) runs with the OS permission jail off, so it must never be reachable in production — any value other than `1` (including absent) keeps it off. Data access is still fully gated by the capability host. | off (disabled) |
| `TREK_PLUGINS_DEV_LINK` | **Development only.** Set to any truthy value (`1`, `true`, `on`, `yes` — any casing) to enable *dev-link*: registering a plugin from a local build directory and hot-reloading it against a live instance's data. Dev-linked code bypasses the install-time signature/integrity checks and (under `npm run dev`) runs with the OS permission jail off, so it must never be reachable in production — absent, blank or an explicitly falsy value (`0`, `false`, `off`, `no`) keeps it off. Data access is still fully gated by the capability host. | off (disabled) |
| `TREK_PLUGIN_AI_PER_DAY` | Per-plugin **daily** cap on shared-LLM broker calls (`ai.complete` / `ai.extract`). Bounds how much a single plugin can spend on the admin's LLM quota per UTC day, independent of its granted permissions. Counts persist across restarts within the same day; set to `0` to disable the AI broker entirely. Generous by design — only bites a runaway plugin. | `200` |
| `TREK_PLUGIN_NOTIFY_PER_DAY` | Per-plugin **daily** cap on user-notification broker calls (`notify.send`), so one plugin can't spam a user. Same UTC-day window and restart-safe counting as `TREK_PLUGIN_AI_PER_DAY`; set to `0` to disable the notify broker. | `100` |
| `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_AUDIT_MAX_ROWS` | Per-plugin retention cap for the capability audit log (kept in the shared `trek.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` |
| `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.
+3 -3
View File
@@ -2,13 +2,13 @@
## Do I need a Google Maps API key?
No. When no Google Maps key is configured, TREK automatically falls back to OpenStreetMap (Nominatim) for place search — no API key or account required. If you want richer place data (photos, ratings, opening hours), an admin can optionally add a Google Maps key in [User Settings](User-Settings).
No. When no Google Maps key is configured, TREK automatically falls back to OpenStreetMap (Nominatim) for place search — no API key or account required. If you want richer place data (photos, ratings, opening hours), an admin can optionally add a Google Maps key in **Admin Panel → Settings** — see [Admin Panel Overview](Admin-Panel-Overview). A key saved there applies instance-wide to every member; there is no per-user field for it in the UI.
## Can I use TREK offline?
Yes. TREK is a Progressive Web App. After your first visit, the service worker (powered by Workbox) caches map tiles (Carto and OpenStreetMap), non-sensitive API responses, uploaded covers and avatars, and all static assets. Subsequent visits work without a network connection for already-cached content. See [Offline Mode and PWA](Offline-Mode-and-PWA) for installation instructions.
Yes. TREK is a Progressive Web App. After your first visit, the service worker (powered by Workbox) caches map tiles (Carto, OpenStreetMap, Mapbox GL and OpenFreeMap), uploaded covers and avatars, and every page of the app. Subsequent visits work without a network connection for already-cached content. Trip data does not come from that cache: it is stored per user in IndexedDB and read back through TREK's own offline layer, with writes queued and replayed once you reconnect. See [Offline Mode and PWA](Offline-Mode-and-PWA) for installation instructions.
> **Note:** Auth, admin, backup, and settings endpoints are intentionally excluded from the offline cache.
> **Note:** API responses are **never** stored in the service-worker cache. Workbox keys its entries by URL and cannot vary them on the session cookie, so caching them would mean one account's data could be served to the next on a shared device.
## How many MCP tokens can I create?
+12 -2
View File
@@ -27,26 +27,36 @@ TREK is a self-hosted, real-time collaborative travel planner licensed under AGP
- **Invite Links** — one-time registration links with configurable max uses 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))
- **Public Share Links** — share a read-only view of any trip
### Addons _(admin-toggleable)_
- **Lists** — packing lists and to-dos with templates, member assignments, optional bag tracking
- **Budget Planner** — expense tracker with category breakdown, splits, multi-currency
- **Costs** — expense tracker with category breakdown, splits, multi-currency
- **Documents** — file manager for trips, places, and reservations
- **Collab** — group chat, shared notes, polls, day-by-day attendance
- **Vacay** — personal vacation day planner with calendar view, public holidays, and carry-over tracking
- **Atlas** — interactive world map, bucket list, travel stats, continent breakdown
- **Collections** — personal place library: save places across trips into named lists, copy them into any trip, share with per-member roles (see [Collections](Collections))
- **Journey** — magazine-style travel journal with entries, photos (via Immich/Synology Photos), maps, and moods
- **Naver List Import** — import places from shared Naver Maps lists
- **MCP** — expose TREK to AI assistants via the Model Context Protocol (OAuth 2.1)
- **AirTrail** — sync flights from your self-hosted AirTrail instance into trips
- **AI Parsing** — LLM fallback that extracts bookings from confirmation files KDE Itinerary cannot read (see [AI-Booking-Import](AI-Booking-Import))
> Dashboard widgets (currency converter and timezone clock) are per-user preferences, not an admin-toggleable addon — see [Dashboard-Widgets](Dashboard-Widgets).
>
> The full addon table, with type and per-addon notes, lives in [Addons-Overview](Addons-Overview).
### Plugins
- **Sandboxed Plugin Runtime** — third-party plugins run in a forked child process with permission-gated RPC, install-time manifest and signature checks, and an egress allowlist (see [Plugins](Plugins))
- **Plugin SDK** — build and publish plugins with the `trek-plugin-sdk` npm package: dev server, preflight, signing, submit (see [Plugin-Development](Plugin-Development))
### AI / MCP Integration
- **MCP Server** — built-in Model Context Protocol server with OAuth 2.1 authentication
- **150+ Tools** — create trips, plan itineraries, manage budgets, send messages, and more
- **30 Resources** — read-only `trek://` URIs for trips, days, places, budget, packing, journeys, and more
- **27 OAuth Scopes** — granular permissions across 13 permission groups
- **29 OAuth Scopes** — granular permissions across 14 permission groups
- **Pre-built Prompts** — `trip-summary`, `packing-list`, and `budget-overview` context loaders
### Admin
+14 -8
View File
@@ -16,7 +16,7 @@ The compose file ships with several hardening options enabled by default:
| `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 |
| `tmpfs: /tmp:noexec,nosuid,size=64m` | Mounts a 64 MB in-memory `/tmp`; required because the container root is read-only |
| `tmpfs: /tmp:noexec,nosuid,size=128m` | Mounts a 128 MB in-memory `/tmp`; required because the container root is read-only |
> **Note (Docker from snap):** If you installed Docker via `snap` (config under `/var/snap/docker/...`), `no-new-privileges:true` will prevent the container from starting with `exec /usr/bin/dumb-init: operation not permitted`. This is a [snap/AppArmor limitation](https://bugs.launchpad.net/snapd/+bug/1908448), not a TREK issue — install Docker from the [official apt repository](https://docs.docker.com/engine/install/ubuntu/) instead, or remove `no-new-privileges`. See [Troubleshooting](Troubleshooting#container-wont-start-exec-usrbindumb-init-operation-not-permitted).
@@ -48,17 +48,23 @@ Docker creates the volumes automatically on first `docker compose up`. Use `dock
## Environment Variables
The compose file reads variables from a `.env` file placed alongside `docker-compose.yml`. At minimum, set:
The compose file has no `env_file:` key, so a `.env` placed alongside `docker-compose.yml` is used only for `${VAR}` interpolation — a value reaches the container only if a line in the `environment:` block references it. Four entries are wired that way:
```bash
# .env
ENCRYPTION_KEY=<output of: openssl rand -hex 32>
TZ=Europe/Berlin
LOG_LEVEL=info
ALLOWED_ORIGINS=https://trek.example.com
APP_URL=https://trek.example.com
```
Uncomment and fill in the OIDC, initial setup, or MCP variables as needed. For a full description of every variable, see [Environment-Variables](Environment-Variables).
The commented `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` lines are interpolated as well, so uncommenting them is enough to pick their values up from `.env`.
Every other variable — `APP_URL`, the OIDC block, `FORCE_HTTPS`, `TRUST_PROXY`, `ADMIN_EMAIL`/`ADMIN_PASSWORD`, the MCP limits — ships as a commented literal. Uncomment the line in `docker-compose.yml` and put the value there; setting it in `.env` alone has no effect.
`APP_URL` is usually not needed. TREK resolves its public base URL as `APP_URL` → first `ALLOWED_ORIGINS` entry → `http://localhost:<PORT>`, so the `ALLOWED_ORIGINS` value above already gives OIDC redirect URIs, passkey origin checks and the links in email notifications the correct origin. Uncomment `APP_URL` only when the public base URL has to differ from the first allowed origin.
For a full description of every variable, see [Environment-Variables](Environment-Variables).
## Image Tags
@@ -67,14 +73,14 @@ Three tag strategies are available:
| Tag | Example | Behavior |
|---|--------------------------|---|
| `latest` | `mauriceboe/trek:latest` | Always the newest release across all major versions |
| Major version | `mauriceboe/trek:3` | Latest release pinned to that major version |
| Full version | `mauriceboe/trek:3.4.0` | Exact release; never changes |
| Major version | `mauriceboe/trek:4` | Latest release pinned to that major version |
| Full version | `mauriceboe/trek:4.0.0` | Exact release; never changes |
The compose file above uses `latest`. To pin, change the `image:` line:
```yaml
image: mauriceboe/trek:3 # track major version 3
image: mauriceboe/trek:3.4.0 # pin to exact release
image: mauriceboe/trek:4 # track major version 4
image: mauriceboe/trek:4.0.0 # pin to exact release
```
## Start TREK
+4 -1
View File
@@ -110,7 +110,7 @@ env:
# TZ: "Europe/Berlin" # timezone for logs, reminders, cron jobs
# LOG_LEVEL: "info" # "info" = concise, "debug" = verbose
# TREK_WIKI_DIR: "/app/wiki" # where /help reads its docs from; leave unset (the image ships them)
# DEFAULT_LANGUAGE: "en" # fallback language on login page; supported: de, en, es, fr, hu, nl, br, cs, pl, ru, zh, zh-TW, it, tr, ar, id, ja, ko, uk, gr
# DEFAULT_LANGUAGE: "en" # fallback language on login page; supported: de, en, es, fr, hu, nl, br, cs, pl, ru, zh, zh-TW, it, tr, ar, id, ja, ko, uk, gr, sv, vi, ca
# ALLOWED_ORIGINS: "https://trek.example.com"
# APP_URL: "https://trek.example.com"
# FORCE_HTTPS: "false" # enable HTTPS redirect + HSTS; requires TRUST_PROXY
@@ -139,10 +139,13 @@ secretEnv:
ADMIN_EMAIL: "" # initial admin email (first boot only)
ADMIN_PASSWORD: "" # initial admin password (first boot only)
OIDC_CLIENT_SECRET: "" # set if using OIDC
UNSPLASH_ACCESS_KEY: "" # optional; free key from unsplash.com/developers
```
Alternatively, use `generateEncryptionKey: true` to let the chart generate and manage the encryption key, or point `existingSecret` / `existingSecretKey` at an existing Kubernetes Secret.
> **Note:** Without `UNSPLASH_ACCESS_KEY` the server queries Unsplash's unauthenticated endpoint, which many datacenter and VPS IP ranges — including a lot of Kubernetes clusters — are blocked or rate-limited on. Trip-cover and place-image search then fail with **"Unsplash search unavailable"**. A free Access Key switches the server to Unsplash's authenticated API (`api.unsplash.com`), which is not subject to that block. The key can also be set in **Admin → Settings → API Keys**; this value takes priority over that one. See [Environment-Variables](Environment-Variables#image-search-unsplash).
### Persistent Storage
```yaml
+9 -5
View File
@@ -28,7 +28,11 @@ Portainer must be installed and connected to your Docker environment. Use **Stac
See https://github.com/liketrek/TREK/blob/main/docker-compose.yml
Set at minimum `ENCRYPTION_KEY`, `TZ`, and `APP_URL` in the **Environment variables** section of the stack editor. Generate an encryption key with:
Set at minimum `ENCRYPTION_KEY`, `TZ`, and `ALLOWED_ORIGINS` in the **Environment variables** section of the stack editor. Portainer's stack variables are only substituted into `${...}` placeholders in the compose file, they are not injected into the container, and the shipped compose file interpolates exactly four: `ENCRYPTION_KEY`, `TZ`, `LOG_LEVEL`, and `ALLOWED_ORIGINS`.
Every other variable ships commented out, so setting it here does nothing. To use one — `APP_URL`, for example, which OIDC needs and which email notification links are built from — uncomment its line in the **Web editor** and put the value there directly, or change it to `- APP_URL=${APP_URL:-}` so the stack variable is picked up.
Generate an encryption key with:
```bash
openssl rand -hex 32
@@ -41,10 +45,10 @@ Three tag strategies are available:
| Tag | Example | Behavior |
|---|---|---|
| `latest` | `mauriceboe/trek:latest` | Always the newest release across all major versions |
| Major version | `mauriceboe/trek:3` | Latest release pinned to that major version |
| Full version | `mauriceboe/trek:3.4.0` | Exact release; never changes |
| Major version | `mauriceboe/trek:4` | Latest release pinned to that major version |
| Full version | `mauriceboe/trek:4.0.0` | Exact release; never changes |
Use `latest` or a major-version tag (e.g. `3`) if you want automatic updates on redeploy. Use a full version tag (e.g. `3.4.0`) if you want explicit control over which release runs.
Use `latest` or a major-version tag (e.g. `4`) if you want automatic updates on redeploy. Use a full version tag (e.g. `4.0.0`) if you want explicit control over which release runs.
## Updating
@@ -54,7 +58,7 @@ How you update depends on the tag you chose:
![Re-pull image and redeploy switch ticked, with arrows pointing to the switch and the Update button](assets/portainer-force-pull.png)
**Pinned full-version tag** — Edit the stack, change the tag in the `image:` line (e.g. `3.4.0` → `3.4.1`), then click **Update the stack**. No need to toggle the re-pull switch — a tag change forces a fresh pull.
**Pinned full-version tag** — Edit the stack, change the tag in the `image:` line (e.g. `4.0.0` → `4.0.1`), then click **Update the stack**. No need to toggle the re-pull switch — a tag change forces a fresh pull.
![Edit stack page with an arrow pointing to the image tag in the compose editor](assets/portainer-update-version.png)
+2 -2
View File
@@ -40,12 +40,12 @@ The Unraid template exposes the following fields in the container UI:
| `TZ` | `UTC` | Timezone for logs, reminders, and scheduled tasks (e.g. `Europe/Berlin`) |
| `ALLOWED_ORIGINS` | *(empty)* | Comma-separated origins for CORS and email notification links, e.g. `https://trek.example.com` |
| `APP_URL` | *(empty)* | Public base URL; required when OIDC is enabled (must match the redirect URI registered with your IdP) |
| `ADMIN_EMAIL` | *(empty)* | Email for the first admin account (first-boot only; no effect once any user exists). Must be set together with `ADMIN_PASSWORD`. |
| `ADMIN_EMAIL` | `admin@trek.local` | Email for the first admin account (first-boot only; no effect once any user exists). Pre-filled by the template — must be set together with `ADMIN_PASSWORD`, otherwise both are ignored. |
| `ADMIN_PASSWORD` | *(empty)* | Password for the first admin account (first-boot only). Must be set together with `ADMIN_EMAIL`. If either is omitted, TREK creates the account with email `admin@trek.local` and a random password printed to the container log. |
### Advanced Variables
Additional variables (`PORT`, `NODE_ENV`, `LOG_LEVEL`, `TREK_WIKI_DIR`, `DEFAULT_LANGUAGE`, `FORCE_HTTPS`, `TRUST_PROXY`, `COOKIE_SECURE`, `ALLOW_INTERNAL_NETWORK`, `SESSION_DURATION`, `SESSION_DURATION_REMEMBER`, all OIDC variables, `MCP_RATE_LIMIT`, `MCP_MAX_SESSION_PER_USER`, `DEMO_MODE`) are available under **Advanced View** in the template editor.
Additional variables (`PORT`, `NODE_ENV`, `LOG_LEVEL`, `TREK_WIKI_DIR`, `DEFAULT_LANGUAGE`, `FORCE_HTTPS`, `HSTS_INCLUDE_SUBDOMAINS`, `TRUST_PROXY`, `COOKIE_SECURE`, `ALLOW_INTERNAL_NETWORK`, `SESSION_DURATION`, `SESSION_DURATION_REMEMBER`, all OIDC variables, `MCP_RATE_LIMIT`, `MCP_MAX_SESSION_PER_USER`, `DEMO_MODE`, `UNSPLASH_ACCESS_KEY`) are available under **Advanced View** in the template editor.
## Setting the Encryption Key
+13 -7
View File
@@ -4,19 +4,25 @@ TREK makes outbound HTTP requests when you configure integrations such as Immich
## Default behavior
All outbound requests go through an SSRF guard (`ssrfGuard.ts`). The guard resolves the hostname to an IP address before allowing the connection and blocks addresses in private ranges.
TREK has two SSRF guards, both in `ssrfGuard.ts`. Which one applies depends on the call site, not on who configured the URL.
**The strict guard** (`safeFetch` / `safeFetchFollow`, built on `checkSsrf`) covers most outbound traffic — Immich, Synology Photos, AirTrail, notification webhooks, ntfy, Unsplash, and place lookups. It resolves the hostname to an IP address before allowing the connection and blocks loopback, link-local and private ranges. Only the private ranges open up, and only with `ALLOW_INTERNAL_NETWORK=true`. The two tables below describe this guard.
**The relaxed guard** (`safeFetchAdminConfigured`, also exported as `safeFetchLlm`) covers endpoints that are expected to live on your own network: OIDC (discovery, token, userinfo, JWKS), the LLM providers behind the AI Parsing addon (a local Ollama or any OpenAI-compatible endpoint), and plugin OAuth token exchanges. It deliberately permits loopback and LAN targets, so a model server on `localhost` or an identity provider on your LAN works **without** `ALLOW_INTERNAL_NETWORK`. It still resolves every hostname, re-checks every redirect hop, and always blocks link-local and cloud-metadata addresses (`169.254.0.0/16`, the full `fe80::/10`, and the AWS and Alibaba metadata addresses).
## Always blocked (no override possible)
These ranges are blocked regardless of any setting:
Under the strict guard, these ranges are blocked regardless of any setting:
| Range | Description |
|---|---|
| `127.0.0.0/8`, `::1` | Loopback |
| `0.0.0.0/8` | Unspecified |
| `169.254.0.0/16`, `fe80::/10` | Link-local / cloud metadata endpoints |
| `169.254.0.0/16`, `fe80::/16` | Link-local / cloud metadata endpoints |
| `::ffff:127.x.x.x`, `::ffff:169.254.x.x` | IPv4-mapped loopback and link-local |
The IPv6 link-local rule here matches the `fe80:` hextet only, which is narrower than the nominal `fe80::/10` prefix (`fe80:`–`febf:`). In practice that is the same set of addresses, since RFC 4291 link-local addresses are always `fe80::/64`. The relaxed guard covers the whole `/10`.
## Blocked unless `ALLOW_INTERNAL_NETWORK=true`
| Range / Hostname | Description |
@@ -27,15 +33,15 @@ These ranges are blocked regardless of any setting:
| `100.64.0.0/10` | CGNAT / Tailscale shared address space |
| `fc00::/7` | IPv6 ULA |
| IPv4-mapped RFC-1918 variants | e.g. `::ffff:10.x`, `::ffff:192.168.x` |
| `*.local`, `*.internal` hostnames | mDNS / internal DNS suffixes (e.g. Docker service names, LAN hosts) |
| `*.local`, `*.internal`, `localhost` hostnames | mDNS / internal DNS suffixes (e.g. Docker service names, LAN hosts) and the literal `localhost` |
The hostname `localhost` is not blocked at the hostname stage, but it resolves to `127.0.0.1` which is caught by the loopback rule above and is therefore always blocked.
The hostname `localhost` is matched at the hostname stage too, but it normally resolves to a loopback address (`127.0.0.1` or `::1`), which the always-blocked loopback rule catches first — so under the strict guard it is blocked no matter how `ALLOW_INTERNAL_NETWORK` is set. On a host that maps `localhost` somewhere else, the hostname rule still applies and it stays blocked unless `ALLOW_INTERNAL_NETWORK=true`. The relaxed guard allows `localhost` outright, which is what makes a local Ollama the supported default for AI Parsing.
`*.local` and `*.internal` hostnames are permitted when `ALLOW_INTERNAL_NETWORK=true` — the guard still resolves them to an IP and enforces all IP-level rules, so any such hostname that resolves to a loopback or link-local address remains blocked regardless.
## When to enable
Set `ALLOW_INTERNAL_NETWORK=true` when Immich, Synology Photos, or another integrated service is hosted on your local network and you need TREK to reach it.
Set `ALLOW_INTERNAL_NETWORK=true` when a service reached through the strict guard — Immich, Synology Photos, AirTrail, or a notification webhook — is hosted on your local network and you need TREK to reach it. You do **not** need it for a local or LAN Ollama, an OpenAI-compatible endpoint, an OIDC provider on your LAN, or plugin OAuth; those go through the relaxed guard and already work. Leave the flag off if only those need internal access, since turning it on widens the surface for every strict-guard integration at once.
See [Environment-Variables](Environment-Variables) for how to set environment variables.
@@ -47,7 +53,7 @@ Even with `ALLOW_INTERNAL_NETWORK=true`, TREK pins the DNS resolution to prevent
## Audit log
When a user saves an Immich URL that resolves to a private IP, TREK records an `immich.private_ip_configured` entry in the [Audit-Log](Audit-Log) including the URL and the resolved IP address. This audit event is specific to Immich; Synology Photos does not emit an equivalent event.
When a user saves an Immich URL that resolves to a private IP, TREK records an `immich.private_ip_configured` entry in the [Audit-Log](Audit-Log) including the URL and the resolved IP address. AirTrail does the same with `airtrail.private_ip_configured`. Synology Photos does not emit an equivalent event.
## See also
+25 -1
View File
@@ -33,7 +33,7 @@ Each entry corresponds to a day in your journey. The entry editor provides:
| `neutral` | Neutral | Grey |
| `rough` | Rough | Violet |
- **Weather** — choose one of six values: Sunny, Partly cloudy, Cloudy, Rainy, Stormy, Cold.
- **Weather** — choose one of six values: Sunny, Partly cloudy, Cloudy, Rainy, Stormy, Snowy. (Snowy is stored under the id `cold`.)
- **Photos** — attach photos to the entry. The first photo becomes the card thumbnail in list views.
> **Note on HEIC files:** HEIC is an Apple-only format that many browsers and platforms do not recognise as an image. To ensure broad compatibility, HEIC/HEIF files are automatically converted to JPEG before upload. This conversion may result in the loss of embedded metadata (EXIF data such as GPS coordinates, camera information, etc.).
- **Pros / Cons** — optional verdict cards. Add items to a **Pros** list (thumbs-up) or a **Cons** list (thumbs-down) to summarise what you loved or what could have been better. These are stored in the `pros_cons.pros` and `pros_cons.cons` arrays on the entry.
@@ -65,6 +65,26 @@ The thin dashed line connecting entries in date order is something else and stay
![Journey detail page for "Autumn in Japan" with its cover header and day/place/entry/photo counts, the day-by-day timeline with Add Entry actions on the left, and the entry map on the right](assets/JourneyDetail.png)
## TREK Studio
A journey can also be laid out as a printable photo book. Open a journey and click **Studio** in the header. The designer opens on top of the journey at `/journey/:id/studio`, so the journey stays open underneath and **Back to the journey** drops you straight back into it. Studio is marked **Beta**.
![The TREK Studio editor with the pages rail on the left, a spread on the workbench and the properties inspector on the right](assets/TREK-Studio-Editor.png)
- **Page format** — Square 21 × 21 cm (the default), Square 30 × 30 cm, A4 landscape, A4 portrait, A5 landscape, or a custom width × height in millimetres. Everything is drawn as a spread (two pages side by side) with 3 mm bleed and a 5 mm safe margin.
- **Auto layout** has two entries. **This spread** builds the spread on screen again from the journal entry it came from, and is only offered on a spread that came from one. **The whole book** replaces every page, keeping your title and page setup. Both are ordinary undo steps, so you can press one, look at it, and undo.
- **Pages, Content, Elements, Travel and Layouts** are the sections of the left rail. Content holds the journey's own photos and entries; Layouts has thirteen spread layouts (Hero and story, Four up, Strip and text, Mosaic and so on) plus a separate set of five for the cover and back; Elements has text styles, shapes, lines, grids, empty frames with their frame styles, and a searchable icon library; Travel builds figures out of the journey itself — route maps, country outlines and lists, flags, date, day and distance marks, and a trip summary.
- **Properties** on the right edits whatever is selected: position and size, crop and focal point, fill or fit, a look filter, corner radius, frame style, stacking order, lock. An element that auto layout tied to a journal entry follows that entry until you edit it here, which breaks the link.
- **Export** opens a print view that your browser turns into a PDF. Pick **Single pages** (one leaf per sheet, what a printer wants) or **Spreads** (two pages at a time, the way the book opens), with optional crop marks that add the bleed on every edge and mark where to cut.
- **Spreads travel between books.** Download the spread you are on as a design file and import it into another book. The file carries the design only, not the photographs.
- **Several people can design at once.** Everyone in the same book sees the others' pointers with their names on them, and a save is pushed to the rest live. A save that lands on a version somebody else has already changed comes back as a conflict, with the other version alongside it, rather than quietly overwriting their work.
![Frame styles and frame shapes in TREK Studio, with a photo's crop, look and frame options in the inspector](assets/TREK-Studio-Frames.png)
The book belongs to its journey and inherits its access exactly: anyone who may read the journey may open its book, and only those who may edit the journey may save or delete it. There is no second set of permissions to keep in step.
Studio is desktop only: it needs a viewport at least 1024 px wide. Below that — a tablet, or a narrowed browser window — it opens over the journey and shows a "Studio needs a bigger screen" notice with a way back. On a phone the journey opens in its mobile view as usual and Studio is not offered at all. Making the PDF is desktop only too.
## Plugin entry rows
Installed plugins can add extra rows to a journal entry card via the `journalEntryProvider` hook. A plugin returns rows (`label`, optional `value`, optional `url`) and TREK renders them natively under the entry — no iframe. Rows are additive and fail-safe: they require the Journey addon, the entry's journey is access-checked the same way as reading it, only http/https/mailto links are allowed, and a provider that errors or is slow is simply skipped.
@@ -75,6 +95,10 @@ Installed plugins can add extra rows to a journal entry card via the `journalEnt
You can share a journey with a read-only public link. When creating the link you can independently toggle which sections are visible to visitors: **Timeline** (entries and stories), **Gallery** (photos), and **Map**. Visitors can only see the sections you have enabled, and no TREK account is required. See [Public-Share-Links](Public-Share-Links) for details on the separate journey share token mechanism.
**Photos appear on the public map too**, as long as **Gallery** and **Map** are both on. A gallery photo that knows where it was taken becomes a thumbnail pin, clustered into one pin where several were taken close together, and sitting below the entry pins so the itinerary stays the point of the map. The location comes from the file's own EXIF for uploads and from Immich or Synology for provider photos, and a photo that carries none simply stays off the map (see the HEIC note above — that conversion drops GPS, so iPhone uploads usually arrive without a location).
Both toggles matter. A photo's coordinates follow the **Map** switch: with Map off the gallery is still shared, but every photo's coordinates are stripped before they leave the server. With Gallery off no photos are sent at all, so a map-only share shows the entry pins and nothing else.
## See also
- [Addons-Overview](Addons-Overview)
+4 -1
View File
@@ -1,6 +1,6 @@
# Languages
TREK ships with translations for 20 languages. You can change your language at any time without logging out.
TREK ships with translations for 23 languages. You can change your language at any time without logging out.
## Supported languages
@@ -26,6 +26,9 @@ TREK ships with translations for 20 languages. You can change your language at a
| `ko` | 한국어 |
| `uk` | Українська |
| `gr` | Ελληνικά |
| `sv` | Svenska |
| `vi` | Tiếng Việt |
| `ca` | Català |
## RTL support
+33 -8
View File
@@ -4,7 +4,7 @@
## Signing in
Navigate to `/login` and enter your email and password. On success, the server sets a `trek_session` cookie — httpOnly, sameSite=`lax`, and secure in production — that persists your session for 24 hours across page reloads and browser restarts. You do not need to sign in again until the session expires or you explicitly log out.
Navigate to `/login` and enter your email and password. On success, the server sets a `trek_session` cookie — httpOnly, sameSite=`lax`, and secure in production — that keeps you signed in across page reloads. How long it survives depends on the **Remember me** switch, described below. You do not need to sign in again until the session expires or you explicitly log out.
> **Note:** The `secure` flag on the cookie can be overridden by setting `COOKIE_SECURE=false` in the server environment (useful for plain-HTTP dev setups), or force-enabled with `FORCE_HTTPS=true`.
@@ -12,17 +12,30 @@ If your account has two-factor authentication enabled, you are prompted for a TO
If you have forgotten your password, click the **"Forgot password?"** link below the password field to start the self-service reset flow. See [Password-Reset](Password-Reset) for details.
### Remember me
The **Remember me** switch below the password field decides how long the session lives:
- **Off** (the default) — a browser-session cookie with no `maxAge`, cleared when you close the browser, backed by a token valid for `SESSION_DURATION` (default `24h`).
- **On** — a persistent cookie matching a token valid for `SESSION_DURATION_REMEMBER` (default `30d`), so the session survives browser restarts.
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`.
### Forced password change
If an admin has marked your account as requiring a password change, a **Set new password** form is shown immediately after a successful login (or after the MFA step). The session cookie is not issued until the new password is saved.
Some accounts are flagged as needing a new password: the admin account TREK seeds on first boot always is, and so is an account restored with the `reset-admin.js` recovery script (see [Troubleshooting](Troubleshooting)). For those accounts a **Set new password** form is shown immediately after a successful login (or after the MFA step). The session cookie has already been issued at that point — the app simply keeps you on the form until the new password is saved, and saving it re-issues the cookie and signs your other sessions out.
There is no control in the Admin Panel that flags an existing account. An admin-set password (see [Admin-Users-and-Invites](Admin-Users-and-Invites)) neither raises the flag nor clears it, so a user who already carries it is still asked to choose their own password at the next sign-in.
## Registering
The Register form appears under one of these conditions:
- **Open registration** — the admin has enabled password registration for the instance (`password_registration` setting).
- **Valid invite link** — you visited `/login?invite=TOKEN` with a valid token (see below).
- **First user** — no accounts exist yet; the registration form is shown automatically and the first account created becomes an admin.
- **Valid invite link** — you visited `/register?invite=TOKEN` with a valid token (see below).
- **No accounts at all** — nobody has signed up yet; the registration form is shown automatically and the first account created becomes an admin. On a normal install this never happens, because TREK seeds an admin on first boot (see [First run](#first-run) below).
Registration fields: **username**, **email**, and **password**.
@@ -42,7 +55,7 @@ Passwords must meet all of the following rules:
### Invite link flow
When an admin shares an invite link (`/login?invite=TOKEN`), visiting it:
When an admin shares an invite link (`/register?invite=TOKEN`), visiting it:
1. Validates the token against the server.
2. Switches the login page to Register mode automatically.
@@ -50,13 +63,25 @@ When an admin shares an invite link (`/login?invite=TOKEN`), visiting it:
If the token is invalid, expired, or exhausted, an error is shown.
## First user
## First run
On a fresh TREK instance with no existing accounts, the registration form opens immediately. The first account created is automatically assigned the **admin** role.
On a normal install TREK seeds an admin account on first boot, before anyone can register, so the login page opens in Sign In mode and the Register link is hidden. The credentials depend on how you start the container:
- With **both** `ADMIN_EMAIL` and `ADMIN_PASSWORD` set, those values are used.
- With only one of the two set, both are ignored (a warning is logged) and the generated credentials below are used instead.
- Otherwise the account is `admin@trek.local`, username `admin`, with a randomly generated password printed to the container log — run `docker logs trek` to retrieve it.
The seeded admin is flagged as requiring a password change, so you are forced to set a new password on first sign-in. Until that happens the Register link stays hidden even though password registration is enabled by default; once the password is changed the link appears normally.
Two setups skip the seeder entirely. `DEMO_MODE=true` seeds the demo data instead (see [Demo-Mode](Demo-Mode)), and on an **OIDC-only** install no local account is created at all — there the first user who signs in through the identity provider is assigned the **admin** role.
See [Quick-Start](Quick-Start) for the full first-boot walkthrough.
## Rate limiting
Failed login attempts are rate-limited to **10 attempts per 15-minute window** per IP address. After exceeding the limit, further attempts return HTTP 429 until the window resets.
Requests to the login endpoint are rate-limited to **10 per 15-minute window** per IP address. The counter is incremented before the credentials are checked, so successful sign-ins count against it too, not only failed ones. After exceeding the limit, further requests return HTTP 429 until the window resets. The window is fixed from the first counted request, and blocked requests do not extend it.
That counter is shared rather than per-endpoint. `POST /api/auth/login`, `POST /api/auth/register`, `GET /api/auth/invite/:token` and the passkey sign-in ceremony each allow 10 requests from it; `PUT /api/auth/me/password`, `POST /api/auth/mfa/disable`, `POST /api/auth/mcp-tokens` and passkey deletion draw on the same counter but are rejected once it reaches 5. Unrelated activity from the same IP therefore eats into the allowance — ten registration attempts will 429 the next login, and five requests of any kind will 429 a password change.
MFA verification attempts are rate-limited separately to **5 attempts per 15-minute window** per IP address.
+43 -1
View File
@@ -18,8 +18,10 @@ Requires `packing:read` or `packing:write` scope.
| `delete_packing_item` | Remove a packing item. |
| `reorder_packing_items` | Set the display order of packing items within a trip. |
| `bulk_import_packing` | Import multiple packing items at once from a list (with optional quantity). |
| `list_packing_templates` | List the reusable packing templates (id, name, item count) so one can be applied. |
| `apply_packing_template` | Apply a saved packing template to a trip. |
| `save_packing_template` | Save the current packing list as a reusable template. |
| `save_packing_template` | Save the current packing list as a reusable template. Templates are global, so this is admin only — a non-admin gets `Admin access required`. |
| `delete_packing_template` | Delete a reusable packing template. Global too, so admin only. |
| `list_packing_bags` | List all packing bags for a trip. |
| `create_packing_bag` | Create a new packing bag (e.g. "Carry-on", "Checked bag"). |
| `update_packing_bag` | Rename or recolor a packing bag. |
@@ -64,6 +66,14 @@ Requires `atlas:read` or `atlas:write` scope.
Requires `collab:read` or `collab:write` scope.
The addon alone is not enough. Collab has four sub-features an admin switches on and off independently in Admin → Addons — Notes, Polls, Chat and What's Next, all on by default — and three of them gate MCP entries. A tool or resource is only registered when its own sub-feature is enabled on top of the addon, so switching one off makes its entries vanish from the tool list rather than return an error. What's Next has no MCP tools or resources of its own, so toggling it changes nothing here.
| Sub-feature | Tools it gates | Resource it gates |
|---|---|---|
| Notes | `create_collab_note`, `update_collab_note`, `delete_collab_note` | `trek://trips/{tripId}/collab-notes` |
| Polls | `list_collab_polls`, `create_collab_poll`, `vote_collab_poll`, `close_collab_poll`, `delete_collab_poll` | `trek://trips/{tripId}/collab/polls` |
| Chat | `list_collab_messages`, `send_collab_message`, `delete_collab_message`, `react_collab_message` | `trek://trips/{tripId}/collab/messages` |
| Tool | Description |
|---|---|
| `create_collab_note` | Create a shared note visible to all trip members. Supports title, content, category, and color. |
@@ -79,6 +89,38 @@ Requires `collab:read` or `collab:write` scope.
| `delete_collab_message` | Delete a chat message (own messages only). |
| `react_collab_message` | Toggle a reaction emoji on a chat message. |
### Collections _(Collections addon required)_
Requires `collections:read` or `collections:write` scope.
| Tool | Description |
|---|---|
| `list_collections` | List the saved-place collections the user owns or has accepted a share for, plus any pending incoming invites. |
| `get_collection` | Get one collection with its members, labels, and all saved places, including the average rating and each member's vote. |
| `available_collection_users` | List users who can still be invited to a collection (excludes current members and guests). |
| `create_collection` | Create a new saved-place collection owned by the user. |
| `update_collection` | Update a collection's name, description, colour, icon, cover, links, or sort order. Owner/admin only. |
| `delete_collection` | Permanently delete a collection and all its saved places. Owner only, and it cannot be undone. |
| `reorder_collections` | Reorder the user's collections — pass every collection id in the desired order. |
| `save_place_to_collection` | Save a place into a collection from a raw payload. Returns a duplicate marker instead of saving when a similar place already exists, unless `force` is true. |
| `save_trip_places_to_collection` | Copy one or more existing trip places into a collection. Duplicates are skipped unless `force` is true. |
| `update_collection_place` | Update a saved place's name, address, coordinates, description, notes, status, category, links, tags, labels, image, or move it to another collection. |
| `set_collection_place_status` | Set a saved place's status: `idea`, `want`, or `visited`. |
| `rate_collection_place` | Set or clear the current user's 1-5 star rating on a saved place. Every member rates independently; pass `null` to remove the vote. |
| `delete_collection_place` | Remove a saved place from its collection. Requires delete permission on the list. |
| `copy_collection_places_to_trip` | Copy one or more saved places into a trip, ratings included. Requires edit access to the target trip. |
| `create_collection_label` | Create a custom per-collection label (name plus optional hex colour) for grouping and filtering places. |
| `update_collection_label` | Rename or recolour a collection label, or change its sort order. |
| `delete_collection_label` | Delete a collection label; its assignments on places are cleared. |
| `assign_collection_labels` | Add labels across a set of saved places, or take them away with `remove=true`. |
| `invite_to_collection` | Invite a user to collaborate on a collection as `viewer`, `editor`, or `admin` (default `editor`). Owner only. |
| `set_collection_member_role` | Change an accepted member's role. Owner only. |
| `remove_collection_member` | Remove an accepted member from a shared collection. Owner only. |
| `cancel_collection_invite` | Cancel a pending invite you sent for a collection. Owner only. |
| `accept_collection_invite` | Accept a pending invite to join a shared collection. |
| `decline_collection_invite` | Decline a pending invite to a shared collection. |
| `leave_collection` | Leave a shared collection you are a member of. The owner cannot leave — delete the list instead. |
### Vacay _(Vacay addon required)_
Requires `vacay:read` or `vacay:write` scope.
+1 -1
View File
@@ -31,7 +31,7 @@ See [MCP-Setup](MCP-Setup) for step-by-step instructions for each method.
## Requirements
- **MCP addon enabled** — an administrator must enable the MCP addon (`mcp`) from the Admin Panel before the `/mcp` endpoint becomes available and the MCP section appears in user settings.
- **`APP_URL` set** — the `APP_URL` environment variable must be configured to your TREK instance's public URL so that OAuth discovery works correctly. Without it, clients that use OAuth 2.1 cannot complete authentication.
- **`APP_URL` set** — set the `APP_URL` environment variable to your TREK instance's public URL so OAuth discovery advertises the right issuer and endpoints. If it is unset or not a valid URL, TREK falls back to the first entry of `ALLOWED_ORIGINS`, and then to `http://localhost:{PORT}`. Only an `https://` origin or `localhost` / `127.0.0.1` is accepted as the issuer — anything else is replaced by `http://localhost:{PORT}`, which remote OAuth clients cannot reach.
## Rate limits and session limits
+5 -3
View File
@@ -6,16 +6,18 @@ OAuth scopes control exactly which data your AI client can read or write in TREK
## All scopes
TREK defines 27 scopes across 13 groups.
TREK defines 29 scopes across 14 groups.
| Group | Scope | Permission |
|---|---|---|
| **Trips** | `trips:read` | View trips, days, day notes, and members |
| | `trips:write` | Create, update, and delete trips, days, day notes, and accommodations; manage members; duplicate trips |
| | `trips:write` | Create and update trips; create, update, and delete days, day notes, and accommodations; manage members; duplicate trips |
| | `trips:delete` | Permanently delete entire trips (irreversible) |
| | `trips:share` | Create, update, and revoke public share links for trips |
| **Places** | `places:read` | Read places, day assignments, tags, and categories |
| | `places:write` | Create, update, and delete places, assignments, and tags |
| **Collections** | `collections:read` | Read saved-place collections, their places, ratings, labels, and members |
| | `collections:write` | Create and edit collections, save, rate, label and copy places, and share lists |
| **Atlas** | `atlas:read` | Read visited countries, regions, and bucket list |
| | `atlas:write` | Mark countries and regions visited, manage bucket list |
| **Packing** | `packing:read` | Read packing items, bags, and category assignees |
@@ -45,7 +47,7 @@ TREK defines 27 scopes across 13 groups.
- `journey:read` or `journey:write` grants journey read access. `journey:share` alone does **not** grant read access — it only enables managing public share links.
- `list_trips` and `get_trip_summary` are always available regardless of scope — they are navigation tools.
- Static tokens and web session JWTs have full access equivalent to all scopes.
- Addon-gated tools (Atlas, Collab, Vacay, Journey) require both the relevant scope **and** the corresponding addon to be enabled by an admin.
- Addon-gated tools (Packing, To-dos, Budget, Collections, Atlas, Collab, Vacay, Journey) require both the relevant scope **and** the corresponding addon to be enabled by an admin. The to-do tools ride the **Packing** addon, not an addon of their own.
## Choosing the right scopes
+6 -6
View File
@@ -4,7 +4,7 @@ This page explains how to connect an AI assistant to your TREK instance. TREK su
![MCP Setup](assets/MCPConfig.png)
> **Cloudflare users:** If your TREK instance is proxied through Cloudflare, Bot Fight Mode and Super Bot Fight Mode will block MCP requests from ChatGPT. Claude.ai is not affected. See [Troubleshooting → MCP requests blocked by Cloudflare WAF](#mcp-requests-blocked-by-cloudflare-waf-bot-fight-mode) for the fix.
> **Cloudflare users:** If your TREK instance is proxied through Cloudflare, Bot Fight Mode and Super Bot Fight Mode will block MCP requests from ChatGPT. Claude.ai is not affected. See [Troubleshooting → MCP requests blocked by Cloudflare WAF](Troubleshooting#mcp-requests-blocked-by-cloudflare-waf-bot-fight-mode) for the fix.
## Option A: OAuth 2.1 (recommended)
@@ -14,7 +14,7 @@ OAuth 2.1 is the preferred connection method. You grant specific scopes during t
Claude.ai (web) supports native MCP connections — no JSON config file required:
1. In TREK, go to **Settings → Integrations → MCP → OAuth Clients** and click **Create**.
1. In TREK, go to **Settings → Integrations → MCP → OAuth 2.1 Clients** and click **New Client**.
2. Select the **Claude.ai** preset. This fills in the redirect URI (`https://claude.ai/api/mcp/auth_callback`) and a default scope set.
3. Give the client a name, adjust scopes if needed, and save. Copy the client ID and client secret (`trekcs_` prefix) — the secret is shown only once.
4. In Claude.ai, open the MCP settings and add a new server using your TREK URL (`https://<your-trek-instance>/mcp`). Claude.ai will open your browser to complete the OAuth consent flow.
@@ -23,7 +23,7 @@ Claude.ai (web) supports native MCP connections — no JSON config file required
Claude Desktop supports native MCP connections — no JSON config file required:
1. In TREK, go to **Settings → Integrations → MCP → OAuth Clients** and click **Create**.
1. In TREK, go to **Settings → Integrations → MCP → OAuth 2.1 Clients** and click **New Client**.
2. Select the **Claude Desktop** preset. This fills in the redirect URI and a default scope set.
3. Give the client a name, adjust scopes if needed, and save. Copy the client ID and client secret — the secret is shown only once.
4. In Claude Desktop, open Settings → MCP and add a new server using your TREK URL (`https://<your-trek-instance>/mcp`). Claude Desktop will open your browser to complete the OAuth consent flow.
@@ -76,7 +76,7 @@ Create a client in TREK using the appropriate preset (Cursor, VS Code, Windsurf,
### Pre-created OAuth clients
**Settings → Integrations → MCP → OAuth Clients** lets you create named OAuth clients before connecting. This gives you:
**Settings → Integrations → MCP → OAuth 2.1 Clients** lets you create named OAuth clients before connecting. This gives you:
- A fixed, named scope list defined up front
- A client secret (`trekcs_` prefix, shown once) for confidential client mode
@@ -94,7 +94,7 @@ Use this when your AI agent or automation script needs to authenticate silently
### Create a machine client
1. Go to **Settings → Integrations → MCP → OAuth Clients** and click **New Client**.
1. Go to **Settings → Integrations → MCP → OAuth 2.1 Clients** and click **New Client**.
2. Tick **Machine client (no browser login)**. The redirect URI field disappears — machine clients don't need one.
3. Give it a name, select scopes, and click **Register Client**.
4. Copy the `client_id` and `client_secret` shown — the secret is displayed only once.
@@ -113,7 +113,7 @@ Machine clients are designed for **AI agent frameworks and custom MCP client imp
> **Deprecated:** Static tokens will stop working in a future version of TREK. Migrate to OAuth 2.1 or machine clients.
Static tokens grant full access to all tools and resources with no scope restrictions. Sessions using a static token will receive deprecation warnings in the AI client on every tool call.
Static tokens grant full access to all tools and resources with no scope restrictions. A static-token session is warned about the deprecation once, not on every call: the notice rides along with the result of the first `list_trips` or `get_trip_summary` in that session — the trip-discovery tools an AI client normally reaches for first — for the client to surface to you. The tool's own payload still comes with it, and every later call in that session returns a plain result. The notice is also part of the session instructions the server sends when the connection initializes.
1. Go to **Settings → Integrations → MCP**, open the **API Tokens** sub-tab, and click **Create New Token**.
2. Give the token a name and copy it immediately — it is shown only once. The token starts with `trek_`.
+11 -5
View File
@@ -2,7 +2,7 @@
TREK exposes **tools** (read and write actions) and **resources** (read-only `trek://` URIs). Tools are registered per-session based on OAuth scopes and enabled addons.
For addon-gated tools (Packing, To-Dos, Atlas, Collab, Vacay, Journey) and their resources, see [MCP-Addon-Tools](MCP-Addon-Tools).
For addon-gated tools (Packing, To-Dos, Atlas, Collab, Collections, Vacay, Journey) and their resources, see [MCP-Addon-Tools](MCP-Addon-Tools).
## Tools
@@ -52,6 +52,7 @@ Requires `places:read` or `places:write` scope.
| `list_places` | List places in a trip, optionally filtered by assignment status, category, tag, or search query. |
| `create_place` | Add a place with name, coordinates, address, category, notes, website, phone, and optional `google_place_id` / `osm_id`. |
| `update_place` | Update any field of an existing place including transport mode, timing, and price. |
| `rate_place` | Set or clear your own 1–5 star rating on a place. Every trip member rates independently and the place shows the average. Pass `null` to clear the vote. |
| `bulk_update_places` | Update many places at once, applying the same field values (e.g. category, price, transport mode) to every listed place in a single call. |
| `delete_place` | Remove a place from a trip. Also removes all day assignments. |
| `bulk_delete_places` | Delete multiple places by ID. Removes all day assignments. Cannot be undone. |
@@ -141,6 +142,11 @@ Requires `budget:read` or `budget:write` scope. Budget addon must be enabled.
| `delete_budget_item` | Remove a budget item. |
| `set_budget_item_members` | Set which members are splitting a budget item (replaces current list). |
| `toggle_budget_member_paid` | Mark or unmark a member as having paid their share. |
| `get_settlement_summary` | Each member's net balance and the suggested payments to settle shared expenses, in the trip's base currency. Call this before recording a settlement. |
| `list_settlements` | List the recorded settle-up payments for a trip — who paid whom, how much, and when. |
| `create_settlement` | Record a settle-up payment: one member paid another the given amount in the trip's base currency. |
| `update_settlement` | Update a recorded settle-up payment (payer, recipient, and amount). |
| `delete_settlement` | Delete a recorded settle-up payment. This is the undo for `create_settlement` and restores the affected balances. |
### Tags
@@ -189,14 +195,14 @@ Resources provide read-only access via `trek://` URIs. Read them to understand c
|---|---|---|
| `trek://trips` | `trips:*` | All trips you own or are a member of |
| `trek://trips/{tripId}` | `trips:*` | Single trip with metadata and member count |
| `trek://trips/{tripId}/days` | `trips:*` | Days of a trip with their assigned places |
| `trek://trips/{tripId}/days` | `trips:read` | Days of a trip with their assigned places |
| `trek://trips/{tripId}/places` | `places:read` | All places in a trip. Supports `?assignment=all\|unassigned\|assigned` |
| `trek://trips/{tripId}/reservations` | `reservations:read` | Flights, hotels, restaurants, and other reservations |
| `trek://trips/{tripId}/days/{dayId}/notes` | `trips:*` | Notes for a specific day |
| `trek://trips/{tripId}/accommodations` | `trips:*` | Hotels and rentals with check-in/out details |
| `trek://trips/{tripId}/days/{dayId}/notes` | `trips:read` | Notes for a specific day |
| `trek://trips/{tripId}/accommodations` | `trips:read` | Hotels and rentals with check-in/out details |
| `trek://trips/{tripId}/members` | `trips:*` | Owner and collaborators |
| `trek://categories` | (any) | Available place categories (id, name, icon, color) |
| `trek://notifications/in-app` | `notifications:read` | Your in-app notifications (most recent 50, unread first) |
| `trek://notifications/in-app` | `notifications:read` | Your in-app notifications (most recent 50, newest first) |
For addon-gated resources (Budget, Packing, To-Dos, Collab, Atlas, Vacay, Journey), see [MCP-Addon-Tools](MCP-Addon-Tools).
+22 -12
View File
@@ -6,14 +6,18 @@ The trip planner map shows your places, route lines, transport overlays, and you
## Map renderer
TREK uses **Leaflet** by default. If you configure a Mapbox access token in Settings → Map, the map upgrades to **Mapbox GL** with higher-quality tiles, 3D buildings, and terrain. If Mapbox GL is selected but no access token is present, TREK falls back to Leaflet automatically so the map is never blank.
TREK uses **Leaflet** by default. The renderer is picked in Settings → Map under **Map provider**: **Leaflet** for raster tiles, **MapLibre GL** for OpenFreeMap vector tiles (no token required), or **Mapbox GL** for vector tiles with 3D buildings and terrain, which additionally needs a Mapbox access token. If Mapbox GL is selected but no access token is present, TREK falls back to Leaflet automatically so the map is never blank.
The scopes required for Mapbox GL are:
- STYLES:TILES
- STYLES:READ
- FONTS:READ
- DATASETS:WRITE
- VISION:CREATE
- DATASETS:READ
- VISION:READ
## Satellite view
A round button in the bottom-left corner of the Leaflet map flips the base layer between the normal map tiles and **satellite** imagery (ESRI World Imagery — no API key needed, usable up to zoom 19). The icon always shows the layer it switches to. Your choice is stored on your account (`map_base_layer`), so it carries over to every trip and survives a reload. Markers, route lines, tracks and booking overlays are drawn on top of either layer.
## Place markers
@@ -28,7 +32,9 @@ When zoomed out, nearby markers are grouped into clusters. Clicking a cluster zo
## Route lines
When you have a day selected, a dark dashed line connects consecutive places in that day's order.
A day's route is drawn as a solid blue line — a bright core over a darker casing, the look Apple Maps uses — through that day's stops in the order you arranged them. It is not on automatically: switch it on with the **Route** toggle on the day, in the day-plan sidebar on desktop or in the day sheet on mobile. The choice is remembered per trip in your browser, and the mobile map turns it on by default the first time you open a trip you have not decided on.
A straight line is drawn immediately, then upgraded to real road geometry from a public OSRM router (or from a plugin route profile), each leg routed in the transport mode that leg carries. If routing fails, that leg stays a straight line between the two stops.
## GPX tracks
@@ -42,7 +48,7 @@ Clicking a line on the map selects that track and opens its details — useful w
### Exporting a trip as GPX
The **GPX** button in the day sidebar's toolbar, next to PDF and ICS, hands the trip back as a `.gpx` file for offline maps such as Organic Maps, for a handheld GPS, or for any other tool that reads the format. On a phone the same download sits in the trip's **Export** sheet, under "More".
The **Export** button in the day sidebar's toolbar opens one dialog with every way a trip leaves TREK: the day plan as a PDF, the bookings as a calendar, and under **Maps & GPS · GPX** the trip as a `.gpx` file for offline maps such as Organic Maps, for a handheld GPS, or for any other tool that reads the format. On a phone the same downloads sit in the trip's **Export** sheet, under "More".
Three scopes:
@@ -52,20 +58,24 @@ Three scopes:
Places carry their description and address, and their category travels along as the GPX symbol, so devices that support it can show a different icon per kind of stop. Elevation is written back for tracks that were imported with it. Exporting is a read, so every trip member can do it, not only those who may edit.
## Route time pills
## Travel times between stops
At zoom level 12 or higher, small pill-shaped labels appear between consecutive places and show the estimated **walking time** and **driving time** for each segment. Below zoom 12 they are hidden to keep the map clean.
Travel times are not drawn on the map — they sit in the day plan. Switch a day's route on and a slim connector row appears between each pair of consecutive stops with that leg's travel time and distance, and an icon for the mode it was routed in: a car for driving, a foot for walking, a bolt for a plugin route profile. On a phone the same rows sit in the day's plan timeline, where they are always shown and need no toggle. If the day has an accommodation and "optimize from accommodation" is on, two extra connectors bookend the day, naming the hotel with the drive out in the morning and back in the evening.
Each leg carries its own mode, so a day routed by car can still have one leg you walk. If you may edit the day, clicking a connector opens the transport-mode menu for that leg alone.
Car and foot times come from a public OSRM router, so they follow real roads and footpaths instead of straight-line estimates. A plugin route profile is answered by the plugin's own route provider instead, which is what lets it fold in things like charging stops; those legs can add a short note next to the distance. When routing is unavailable the leg falls back to a straight line and shows no time.
## Reservation and transport overlay
Flights, trains, cars, and cruises can be drawn as overlays between their endpoint places. Overlays are **off by default** — activate each reservation individually by clicking the small **Route** icon next to the booking row in the day sidebar, or use one of the bulk options below. The selection is remembered per trip in your browser. Click the icon again to hide it.
Flights, trains, cars, and cruises can be drawn as overlays between their endpoint places. Overlays are **off by default** — activate each reservation individually by clicking the small **Route** icon next to the booking row in the day sidebar, or use one of the bulk options below (automated public-transit journeys are the exception — see the transit bullet). The selection is remembered per trip in your browser. Click the icon again to hide it.
- **Flights and cruises** — geodesic great-circle arcs
- **Flights, cruises and ferries** — geodesic great-circle arcs
- **Cars, buses, taxis and bicycles** — real routed lines that follow actual roads, fetched on demand from a public OSRM router (driving for car/bus/taxi, cycling for bicycle). A straight line is shown while the route loads and kept if routing fails or the trip is very long (~2000 km+)
- **Trains** — a straight line between the endpoints; a multi-leg train draws its whole station chain (from → stop → to)
- **Automated public transit** — a journey added from the transit search draws its real rail and bus alignment instead of a straight line: each ride leg in its line's own colour over a white casing, walking transfers as a dotted grey line. A journey whose provider sent no shape falls back to a straight line. Unlike every other type it has no per-booking **Route** icon in the day sidebar — it is drawn when the day's own **Route** toggle is on and the journey runs on that day, and independently of that by the bulk button, the account-wide default, or the **On map** button in the journey's detail sheet on a phone. Because those are two separate gates, **hide all** does not clear a transit journey while that day's **Route** toggle is still on
- **Antimeridian crossings** — routes that cross the date line now draw as one continuous arc instead of splitting into disconnected segments at the map edges
- **Endpoint markers** — pill-shaped labels with the transport icon and the endpoint code (e.g. IATA airport code) or location name
- **Flight stats** — a floating label on the arc shows departure code → arrival code and, when times are available, the duration and great-circle distance. Stats labels are only rendered for flights.
- **Confirmed reservations** — solid line; **Pending** — dashed line
**Bulk options**, alongside the per-booking toggle:
@@ -73,7 +83,7 @@ Flights, trains, cars, and cruises can be drawn as overlays between their endpoi
- **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.
- **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.
> **Admin:** Whether endpoint text labels appear on the endpoint markers is controlled by the **Booking route labels** setting in Settings → General (`map_booking_labels`).
> **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.
## Plugin map markers
@@ -99,6 +109,6 @@ If geolocation is denied or unavailable, the button turns red.
Right-click anywhere on the **Leaflet** map to open the Place form with the clicked coordinates and a reverse-geocoded address already filled in.
On the **Mapbox GL** map, right-click is reserved for the built-in rotate/pitch gesture, so use **middle-click** instead to trigger the same Place form.
The **Mapbox GL** and **MapLibre GL** maps take the same right-click, and additionally **middle-click** and a **long-press** on touch. A right-button drag that rotates or pitches the map is not mistaken for a click, so the gesture and the shortcut coexist.
**See also:** [Places-and-Search](Places-and-Search) · [Day-Plans-and-Notes](Day-Plans-and-Notes) · [Route-Optimization](Route-Optimization) · [Map-Settings](Map-Settings) · [Reservations-and-Bookings](Reservations-and-Bookings)
+1 -1
View File
@@ -72,7 +72,7 @@ You can also enter a custom `mapbox://styles/USER/ID` URL directly.
### 3D Buildings & Terrain
Enables pitch and building extrusions on all styles. Terrain elevation (DEM-based height) is additionally applied on satellite styles (`Satellite` and `Satellite Streets`). On non-satellite styles only building extrusions are added; terrain is intentionally omitted on those styles because the elevation data would cause route lines to visually drift away from the HTML place markers.
Enables pitch and 3D buildings on all styles. `Mapbox Standard` and `Standard Satellite` ship their own building volumes, so TREK adds no extrusion layer there; every other style gets one injected. Terrain elevation (DEM-based height) is additionally applied on `Satellite`, `Satellite Streets` and `Outdoors`; `Standard Satellite` brings its own terrain from Mapbox and is left alone. The remaining styles get buildings but no terrain — including plain `Mapbox Standard`, whose built-in terrain TREK switches off — because the elevation data would cause route lines to visually drift away from the HTML place markers.
### High Quality Mode *(Experimental)*
+9 -7
View File
@@ -8,7 +8,7 @@ The dashboard at `/dashboard` is your home base — it lists all your trips, let
Use the toggle button in the top toolbar to switch between **grid** (card thumbnails) and **list** (compact rows). Your preference is saved in `localStorage` under the key `trek_dashboard_view` and persists across sessions.
In grid mode the dashboard shows a large [Spotlight card](#spotlight-card) for your most relevant trip, with remaining trips in a responsive grid below. In list mode the Spotlight card is not shown separately — all trips (including the one that would be the spotlight) appear as uniform rows in the same sort order.
In both modes the dashboard shows a large [Spotlight card](#spotlight-card) for your most relevant trip at the top of the page — the toggle only changes how the trips below it are laid out. The spotlighted trip is taken out of that list when it earns the spot on its own: an ongoing trip, or the next upcoming one. If you have neither, the spotlight borrows your first trip for the header and that trip still shows up in the list below.
## Sort Order
@@ -28,15 +28,17 @@ If you have no trips yet, the spotlight card is not shown.
## Archived Trips
Archived trips are hidden from the main list and collapsed into a separate **Archived** section at the bottom of the page. Click the section header to expand it. You can **Copy**, **Restore**, or permanently **Delete** an archived trip from the row actions.
Archived trips are kept out of the default list. The trips section header carries a **Planned / Archived / Completed** filter and the grid shows exactly one of those three sets — switch to **Archived** to see them. On desktop an archived card has the same four actions as any other card, with **Archive** turned into **Restore**. On mobile an archived card drops **Edit**: you can **Copy**, **Restore**, or permanently **Delete** the trip.
## Greeting (Mobile)
## Mobile Header
On mobile, the header shows a time-of-day greeting — "Good morning", "Good afternoon", or "Good evening" — along with your username and avatar. The greeting changes at 12:00 (noon) and 18:00. The mobile header also includes a **Notifications** button (bell icon) that navigates to `/notifications`.
On mobile, the header is a floating glass bar: the TREK logo on the left (tap it to scroll back to the top), a **Notifications** button (bell icon, with a dot when something is unread) that navigates to `/notifications`, and your avatar on the right. The avatar opens the user menu — settings, the admin panel for admins, the dark/light/auto theme cycle, and sign out.
## Dashboard Widgets Sidebar
On wide screens a sticky right column shows the **Currency Converter** and **Timezone Clock** widgets. Each can be toggled on or off via the Settings icon in the toolbar. On mobile, the widgets are available as a bottom sheet from the quick-action buttons at the top of the page.
On wide screens a sticky right column shows the **Currency**, **Collections** (only with the Collections addon enabled), **Timezones**, and **Upcoming reservations** widgets, plus any widget a plugin contributes. Each one is switched on or off separately for desktop and mobile under **Settings → Appearance → Dashboard widgets**; desktop also has a **Right sidebar** master toggle that removes the whole column, and the column disappears on its own once every widget in it is off.
On mobile, the same widgets are inline panels below the spotlight card, not a bottom sheet. By default the trip list comes first and the widgets stack underneath it. How they stack — and where the trip list sits among them — is set on the phone under **Settings → Appearance → Mobile → Dashboard order**.
See [Dashboard-Widgets](Dashboard-Widgets) for full usage details.
@@ -51,11 +53,11 @@ On desktop, hover over a card (or open the row actions in list view) to reveal t
| **Archive / Unarchive** | `trip_archive` on that trip |
| **Delete** | `trip_delete` on that trip |
Actions not permitted for your role are hidden. Admins always see all actions.
Which actions appear depends on layout and archive state, not on your role — the dashboard draws the buttons without checking permissions. On mobile a grid card leaves **Archive** out (it lives in the edit sheet) while a list row includes it. The permissions above are enforced by the server: an action your role is not allowed to perform comes back as a 403 and you get an error message instead of the button being hidden. Permissions do take effect inside the trip dialog — without `trip_cover_upload` the cover-image panel is not shown, and without `trip_edit` the title, description, and currency fields are read-only.
## Empty State
When you have no trips, the dashboard shows an illustration and a **Plan your first trip** button that opens the [Creating-a-Trip](Creating-a-Trip) dialog.
When you have no trips, the dashboard shows the TREK mascot with the caption **No trips yet**. Start one from the **New Trip** button in the bottom-right corner or the dashed **New Trip** tile at the end of the grid; both open the [Creating-a-Trip](Creating-a-Trip) dialog. On mobile the empty state itself carries a **Create First Trip** button.
## Related Pages
+6 -3
View File
@@ -19,7 +19,7 @@ TREK ships four delivery channels, and a plugin can add more. Which channels app
A plugin can register an **additional channel** — Gotify, Pushover, Telegram, anything that
takes a message — by implementing the `notificationChannel` hook. See
[Plugin-Development](Plugin-Development.md#notification-channels).
[Plugin-Development](Plugin-Development#notification-channels).
Once the admin installs the plugin and switches its channel on, it appears as a new column in
the preferences matrix beside Email and In-App, and behaves like any other channel: each user
@@ -43,8 +43,10 @@ The following events are configurable in user settings:
| `trip_invite` | Someone invited you to a trip |
| `booking_change` | A booking was added, updated, or removed in a trip you're part of |
| `trip_reminder` | Reminder before a trip starts |
| `todo_due` | A to-do assigned to you, or in a trip you're part of, is due soon |
| `vacay_invite` | You were invited to fuse vacation plans |
| `vacay_share` | Someone shared their vacation calendar with you (view only) |
| `collection_invite` | Someone invited you to share a collection |
| `photos_shared` | Photos were shared with a trip |
| `collab_message` | A new message in a collaborative trip |
| `packing_tagged` | You were assigned to a packing category in a trip |
@@ -59,10 +61,11 @@ The following events are shown in the admin panel (Admin → Notifications) and
| Event | Description | Channels |
|-------|-------------|---------|
| `version_available` | A new TREK version is available | in-app, email, webhook, ntfy |
| `replica_failure` | A write to a storage replica failed (repeats within the hour are suppressed and counted) | in-app, email, webhook, ntfy |
### System-only events
### In-app-only events
The following events are fired automatically and are not exposed as toggles in any settings panel:
The following event is fired automatically and can only be delivered in-app — it appears in the preferences matrix with an In-app toggle and a dash in every other column:
| Event | Description | Channels |
|-------|-------------|---------|
+13 -3
View File
@@ -15,13 +15,23 @@ OpenID Connect (OIDC) lets users log in with an existing identity provider — G
5. The server issues a short-lived one-time code and redirects your browser to `/login?oidc_code=<code>`. The frontend immediately exchanges that code at `GET /api/auth/oidc/exchange?code=<code>` to obtain the session.
6. Your `trek_session` cookie is set and you land on the dashboard.
**Remember me.** The Remember-me switch on the login page is carried into SSO as a query flag on the login URL: `GET /api/auth/oidc/login?remember=1` (or `remember=0`), appended with `&` when an invite token is already present. Only `0` and `1` are accepted; any other value — and starting SSO from the register tab, where the switch is not shown — is treated as if the parameter were absent. The choice is held in the server-side login state, survives the provider round-trip and the one-time-code exchange, and decides both the session lifetime and the cookie lifetime:
| `remember` | Session lifetime | `trek_session` cookie |
|---|---|---|
| `1` | `SESSION_DURATION_REMEMBER` (default `30d`) | persistent, `maxAge` matches |
| `0` | `SESSION_DURATION` (default `24h`) | browser-session cookie, cleared when the browser closes |
| omitted | `SESSION_DURATION` (default `24h`) | persistent, `maxAge` matches |
If SSO sessions are dying at browser close, the login link is sending `remember=0`.
## Prerequisites
Set the following environment variables before starting the server:
| Variable | Required | Description |
|---|---|---|
| `APP_URL` | Yes | Base URL of your TREK instance (e.g. `https://trek.example.com`). Used to build the redirect URI. Can also be set via Admin → Settings. |
| `APP_URL` | Yes | Base URL of your TREK instance (e.g. `https://trek.example.com`). Used to build the redirect URI. **Env var only — not configurable via the admin panel.** If it is unset (or not a parseable URL, in which case it is ignored), TREK falls back to the first `ALLOWED_ORIGINS` entry, and only if that one is missing or unparseable to `http://localhost:{PORT}` — which produces a redirect URI your IdP will reject. A scheme-less `ALLOWED_ORIGINS=trek.example.com` does not parse, so it ends up on localhost too. |
| `OIDC_ISSUER` | Yes | Issuer URL of your identity provider. Must use HTTPS in production. |
| `OIDC_CLIENT_ID` | Yes | OAuth 2.0 client ID registered with your IdP. |
| `OIDC_CLIENT_SECRET` | Yes | OAuth 2.0 client secret. |
@@ -47,7 +57,7 @@ For example: `https://trek.example.com/api/auth/oidc/callback`
## New-user registration via SSO
When an SSO login matches an existing TREK account by email or OIDC subject (`sub`), that account is used and the OIDC identity is linked automatically. If no matching account exists, TREK attempts to create one. The outcome depends on the following:
When an SSO login matches an existing TREK account by OIDC subject (`sub`), that account is used directly. When it matches only by **email**, the OIDC identity is linked to that account only if the provider asserts `email_verified` for it; if the claim is missing or false the login is rejected with an `email_not_verified` error, so an unverified address can never take over a local account. Make sure your IdP includes `email_verified` in the userinfo response — it is part of the `email` scope. If no matching account exists, TREK attempts to create one. The outcome depends on the following:
- **First user ever**: always created as admin, no invite required.
- **Open SSO registration enabled** (admin panel toggle `oidc_registration`): account is created as a regular user.
@@ -56,7 +66,7 @@ When an SSO login matches an existing TREK account by email or OIDC subject (`su
## Admin panel (runtime configuration)
OIDC can also be configured without environment variables via **Admin → SSO**. The following fields are settable at runtime:
OIDC can also be configured without environment variables via **Admin → Settings**, in the **Single Sign-On (OIDC)** card. The following fields are settable at runtime:
| Field | Env var equivalent |
|---|---|
+6 -5
View File
@@ -29,8 +29,9 @@ TREK uses Workbox service-worker caching plus an IndexedDB database (Dexie) for
| Content | Cache name | Strategy | Duration | Max entries |
|---------|------------|----------|----------|-------------|
| CartoDB / OpenStreetMap map tiles | `map-tiles` | CacheFirst | 30 days | 12 288 |
| Mapbox GL style, glyphs, sprites and vector tiles | `mapbox-tiles` | StaleWhileRevalidate | 30 days | 3 000 |
| OpenFreeMap MapLibre style and tiles | `openfreemap-tiles` | StaleWhileRevalidate | 30 days | 3 000 |
| Mapbox GL and OpenFreeMap style documents | `gl-map-styles` | NetworkFirst (5 s timeout) | 30 days | 20 |
| Mapbox GL glyphs, sprites and vector tiles | `mapbox-tiles` | StaleWhileRevalidate | 30 days | 3 000 |
| OpenFreeMap MapLibre glyphs, sprites and vector tiles | `openfreemap-tiles` | StaleWhileRevalidate | 30 days | 3 000 |
| Cover images and avatars (`/uploads/covers`, `/uploads/avatars`) | `user-uploads` | CacheFirst | 7 days | 300 |
| App shell and every page of the app (HTML / JS / CSS) | precache | Precached | Until next deploy | — |
@@ -43,8 +44,8 @@ TREK uses Workbox service-worker caching plus an IndexedDB database (Dexie) for
On login, after each trip-list refresh, and on WebSocket reconnect, TREK runs a background sync that writes full trip bundles into IndexedDB:
- Trips, days, places, packing items, to-dos, budget items, reservations, accommodations, trip members, tags, and categories.
- Non-photo file attachments (PDFs, documents, etc.) are downloaded and stored as blobs in IndexedDB.
- Map tiles are pre-fetched into the service-worker `map-tiles` cache for zoom levels 10–16 across each trip's bounding box (capped at ~50 MB of tiles per sync).
- 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).
**Sync scope and eviction**
@@ -82,7 +83,7 @@ The stats panel shows cached trips, pending changes, conflicts and failed change
- 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.
- Photo uploads require connectivity; non-photo file attachments are pre-cached automatically during sync.
- 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.
+11 -37
View File
@@ -1,6 +1,6 @@
# PDF Export
TREK can generate two kinds of PDFs from your trip data: a structured **Trip Plan PDF** and a photo-book-style **Journey Book PDF**. Both render as HTML in a sandboxed iframe and open the browser's native print/save dialog — no server-side processing is involved.
TREK generates a structured **Trip Plan PDF** from your trip data. It renders as HTML in a sandboxed iframe and opens the browser's native print/save dialog — no server-side processing is involved. Journey entries no longer go through a fixed PDF template: they are laid out in **TREK Studio** and printed through the same browser mechanism (see below).
![PDF Export](assets/PDFTrip.png)
@@ -10,7 +10,7 @@ TREK can generate two kinds of PDFs from your trip data: a structured **Trip Pla
### How to generate
Open the Day Plan sidebar in the trip planner. Click the **PDF** button in the toolbar at the top of the sidebar. A preview modal opens immediately; click **Save as PDF** to open your browser's print dialog and save the file.
Open the Day Plan sidebar in the trip planner and click the **Export** button in the toolbar at the top of the sidebar. The Export dialog opens with three groups — **Document**, **Calendar**, and **Maps & GPS**. Pick the PDF row under **Document**: a preview modal opens, and **Save as PDF** hands the document to your browser's print dialog.
### Cover page
@@ -21,11 +21,13 @@ Open the Day Plan sidebar in the trip planner. Click the **PDF** button in the t
- **Days** — total number of days in the trip
- **Places** — total places in your trip's place list
- **Planned** — number of unique places assigned to at least one day
- **Estimated cost** — sum of all assigned place prices, shown in the trip's currency (hidden if zero)
- **Cost** — sum of all assigned place prices, shown in the trip's currency (hidden if zero). Mixed currencies are converted at current rates and prefixed with "≈"; when a rate is missing, the tile shows a per-currency breakdown instead
### Per-day pages
Each day starts on a new page with a dark header bar showing the day number, day title, date, and the day's estimated cost.
Each day starts on a new page (unless you turn off **Page break per day**) with a dark header bar showing the day number, day title, date, and the day's estimated cost.
The **Page break per day** toggle sits in the preview modal's header, next to Save as PDF. It is on by default; turn it off and the days flow into each other instead, which is worth doing on a trip of short days that would otherwise print one sheet per handful of lines. The choice is remembered in that browser for the next export.
Below the header:
@@ -51,47 +53,19 @@ Installed plugins can append their own sections to the Trip Plan PDF via the `pd
---
## Journey Book PDF
## Journey photo books
### How to generate
The Travel Journal has no fixed-template PDF export any more. Open a Journey entry and click **Studio** in the journal header (the book icon in the top bar on phones) to open **TREK Studio**, the photo-book designer.
Open a Journey entry in the Travel Journal. Click the **download icon** button in the journal's header area. A preview modal opens; click **Save as PDF** to print.
Studio lays the journey out as editable spreads instead of a fixed page template: five page presets (210 mm and 300 mm square, A4 landscape, A4 portrait, A5 landscape) or a custom size between 60 and 500 mm, and seven bundled font families.
![Journey Book PDF Preview](assets/PDFJourney.png)
### Format
A4 landscape (`@page { size: A4 landscape; margin: 0 }`). Font: Inter, loaded from Google Fonts.
### Cover page
- Hero image (journey cover image, or the first entry photo if none is set)
- Journey title and optional subtitle
- Stat tiles: Days, Entries, Photos
### Entry pages
One page per journal entry, in chronological order. The first entry of each date carries a day header (day number and full date) above the content.
Photo layout adapts to the number of photos on the entry:
| Photos | Layout |
|--------|--------|
| 1 | Single image, full width |
| 2 | Two images side by side |
| 3 or more | Large hero image on the left, two stacked images on the right |
Below the photos: entry time and location, entry title, journal text (rendered from Markdown), and pros/cons verdict cards if present.
### Closing page
A dark "The End" card.
Printing works the same way as the Trip Plan PDF — the sheets are written into a sandboxed `srcdoc` iframe and handed to the browser's print dialog, so nothing is rendered on the server. A browser writes no TrimBox or BleedBox, so the sheets can carry crop marks for a print shop instead. **Single pages** or **Spreads**, and crop marks on or off, are chosen in Studio's export panel.
---
## How rendering works
Both PDFs use the same mechanism: the HTML document is written into a sandboxed `<iframe>` via `srcdoc`, and `iframe.contentWindow.print()` opens the browser's print dialog. There is no server-side PDF generation. The file is saved through the browser's built-in "Save as PDF" print destination.
The Trip Plan PDF and Studio's print view use the same mechanism: the HTML document is written into a sandboxed `<iframe>` via `srcdoc`, and `iframe.contentWindow.print()` opens the browser's print dialog. There is no server-side PDF generation. The file is saved through the browser's built-in "Save as PDF" print destination.
---
+36 -12
View File
@@ -6,13 +6,13 @@ Create categorized packing checklists with member assignments and optional bag t
## Where to find it
Open the **Lists** tab inside the trip planner and select **Packing**. The tab is only visible when the Packing addon is enabled.
Open the **Lists** tab inside the trip planner and select **Packing**. The tab is only visible when the Lists addon is enabled.
> **Admin:** Enable the Packing addon and optionally turn on Bag Tracking in [Admin-Addons](Admin-Addons).
> **Admin:** Enable the **Lists** addon (its internal id is `packing`, and it covers both Packing and Todo) and optionally turn on its nested **Bag Tracking** toggle in [Admin-Addons](Admin-Addons).
## Progress bar
A progress bar shows how many items have been checked (packed) out of the total. It is hidden on small screens and visible on larger viewports. When all items are checked, a completion message replaces the bar.
A progress bar shows how many items have been checked (packed) out of the total. It is hidden on small screens and visible on larger viewports. When all items are checked, an **All packed!** message replaces the packed/total counter and the bar turns green.
## Filters
@@ -24,14 +24,18 @@ Three filter buttons let you narrow the item view:
## Categories
Items are grouped into categories. Each category has a colored dot that cycles through a 10-color palette. When you create a new packing list, suggested items are pre-populated in these categories: **Documents** (Passport, Travel Insurance, Visa Documents, Flight Tickets, Hotel Bookings, Vaccination Card), **Clothing** (T-Shirts (5x), Pants (2x), Underwear (7x), Socks (7x), Jacket, Swimwear, Sport Shoes), **Toiletries** (Toothbrush, Toothpaste, Shampoo, Sunscreen, Deodorant, Razor), **Electronics** (Phone Charger, Travel Adapter, Headphones, Camera, Power Bank), **Health** (First Aid Kit, Prescription Medication, Pain Medication, Insect Repellent), and **Finances** (Cash, Credit Card).
Items are grouped into categories. Each category has a colored dot that cycles through a 10-color palette. A new packing list starts empty — nothing is pre-populated, and the panel just reads *Packing list is empty* until you add something. Fill it by adding categories and items by hand, by applying a saved template (see [Templates](#templates)), or by pasting a whole list in at once (see [Importing a list](#importing-a-list)).
The UI labels these groups **lists** rather than categories: the button that creates one reads **Add list** with the placeholder *List name (e.g. Clothing)*, moving an item between them reads **Move to List**, and the confirm dialog asks about deleting *the list "{name}"*. The REST API and this page still call them categories.
Each category header has a collapse/expand toggle and an overflow menu with these actions:
- **Check all** — mark every item in the category as packed.
- **Uncheck all** — unmark every item in the category.
- **Rename** — rename the category.
- **Delete** — delete the category and all its items.
- **Check All** — mark every item in the category as packed.
- **Uncheck All** — unmark every item in the category.
- **Delete List** — delete the category and all its items.
**Rename** and **Delete List** need the `packing_edit` permission; **Check All** and **Uncheck All** are always shown.
### Assigning members to a category
@@ -46,10 +50,29 @@ 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.
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).
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.
## Importing a list
**Import** in the Lists header opens a paste box, or use **Load CSV/TXT** to pick a file. One item per line:
```
Category, Name, Weight in g (optional), Bag (optional), checked/unchecked (optional)
```
```
Toiletries, Toothbrush
Clothing, T-Shirts, 200
Documents, Passport, , Carry-on
Electronics, Charger, 50, Suitcase, checked
```
Commas, semicolons and tabs all work as separators, and a quoted value keeps its commas (`"Shirt, blue"`). A line with a single value is treated as just a name. A line without a category lands in **Other**, and a bag name that does not exist yet is created for you. Imported items are appended; nothing already on the list is removed.
Import is the only place in the UI that loads weights and bag assignments in bulk — applying a template brings across names and categories only. It requires the `packing_edit` permission.
## Sharing packing items
Every packing item has a sharing tier that controls who sees it and who is bringing it. By default everything sits in the shared group pool, exactly as before — the tiers are opt-in per item.
@@ -97,7 +120,8 @@ Bag tracking is only available when an admin has enabled it.
When enabled, a **Bags** panel appears as a right-hand sidebar on wide screens, or as a modal sheet on narrow screens (tap the **Bags** button in the header to open it). Each bag shows:
- Name and color dot.
- Total weight and a weight-limit progress bar (if a limit is set).
- Total weight, and a weight limit if you set one. Click **Set limit** next to the weight (or the limit itself, to change it) and type the limit in kilograms — that is how airlines state them, and TREK stores it in grams. Clearing the field removes the limit again.
- A fill bar. With a limit it reads against that limit; without one the bag is scaled against the heaviest bag, so bags stay comparable.
- Member avatars assigned to that bag.
- Item count.
@@ -105,7 +129,7 @@ The sidebar also shows an **unassigned** section for items that have no bag, and
To use bags:
1. Click **+ Add bag** to create a bag with a name and color.
1. Click **+ Add bag** and type a name. The color dot is assigned automatically from a fixed palette; there is no color picker.
2. Assign items to a bag using the bag picker on each item row (visible when bag tracking is enabled).
3. Assign members to a bag using the member chip row on the bag card.
@@ -113,14 +137,14 @@ To use bags:
You can save and reuse packing lists across trips:
- **Save as Template** — click the **Save as Template** button in the header (visible when the list has items) to save the current list's items and categories as a named template.
- **Save as template** (admins only) — click the **Save as template** button in the header to save the current list's items and categories as a named template. It only shows for instance admins, and only when the list has items.
- **Apply Template** — if templates exist, an **Apply Template** dropdown appears in the header. Selecting a template appends its items to the current list without removing existing items.
Templates are managed by admins in [Admin-Packing-Templates](Admin-Packing-Templates).
## Permissions
All write operations require the `packing_edit` permission.
All write operations require the `packing_edit` permission. Saving a list as a template on top of that requires an instance admin account; applying a template does not.
## See also
+2 -2
View File
@@ -16,9 +16,9 @@ The Apply Template button only appears when at least one template exists and you
## Saving the current list as a template
In the packing panel toolbar, click the **Save as template** button (folder-plus icon) when items exist in the list. An inline name input appears in the toolbar — type a name and press **Enter** or click the confirm button. The current trip's categories and items are saved as a new reusable template.
In the packing panel toolbar, click the **Save as template** button (folder-plus icon) when items exist in the list. An inline name input appears in the toolbar — type a name and press **Enter** or click the confirm button. The template captures the **Shared** pool plus your own items — other members' Personal items, and the items they shared with specific people, are deliberately left out. Only each item's name and category are stored; quantities, weights, bag assignments and checked state are not.
The Save as Template button only appears when there are items in the list and you have `packing_edit` permission.
The Save as Template button only appears when there are items in the list, you have the `packing_edit` permission, and you are an instance admin. Non-admins can apply templates but not create them; the API answers a non-admin with `403 Admin access required`.
> **Admin:** Templates are created and managed in [Admin-Packing-Templates](Admin-Packing-Templates). Each template has a three-level structure: template → categories → items.
+1 -1
View File
@@ -59,7 +59,7 @@ Passkeys are bound to a domain (the *Relying Party ID*). If no usable domain res
Both values can be pinned with environment variables, which take priority over the Admin panel fields:
- `WEBAUTHN_RP_ID` — the registrable domain, e.g. `trek.example.com`. Bare IP addresses are rejected.
- `WEBAUTHN_RP_ID` — the registrable domain, e.g. `trek.example.com`. A pinned value is used verbatim, so it has to be a real domain: browsers refuse a bare IP address as a Relying Party ID. TREK checks for one only on the RP ID it derives from `APP_URL` — an IP host there resolves to nothing and the feature reports itself as not configured.
- `WEBAUTHN_ORIGINS` — comma-separated allowed origins, e.g. `https://trek.example.com`.
See [Environment-Variables](Environment-Variables) for the full description of both.
+6 -4
View File
@@ -35,21 +35,23 @@ The email delivery uses the same SMTP settings as other notification emails. See
### OIDC accounts
Accounts that signed up via SSO and have no local password set cannot use the forgot-password flow — there is no local password to reset. The forgot-password page still shows the generic confirmation to avoid revealing whether the email is OIDC-only. Continue using [OIDC-SSO](OIDC-SSO) to sign in.
Any account linked to an identity provider cannot use the forgot-password flow — one created by SSO, but equally a local account that was auto-linked to the IdP on its first SSO login. The forgot-password page still shows the generic confirmation to avoid revealing whether the email is SSO-linked. Continue using [OIDC-SSO](OIDC-SSO) to sign in; a local password can still be set by an admin, or by you from **Settings → Account** if you know your current one.
### Password login disabled
If the admin has globally disabled password login, the forgot-password endpoint returns an error and the flow is unavailable.
If the admin has globally disabled password login, no reset link is ever issued. For enumeration safety the endpoint still answers with the same generic confirmation, so the page looks exactly as it always does — the email (or the console line) simply never arrives. The refusal is recorded in the audit log as `user.password_reset_request` with `reason: password_login_disabled`.
## Admin-initiated reset
An admin can set a new password for any user directly from the admin panel (**Admin → Users**). The admin enters a new password for the account, which is saved immediately — no email is required. The admin can also enable the **"Force password change on next login"** flag so the user is prompted to choose their own password the next time they sign in.
An admin can set a new password for any user directly from the admin panel (**Admin → Users**, edit the user). Leaving the **New Password** field empty keeps the current password; entering one saves it immediately — no email is required. Setting a password this way also bumps the account's `password_version`, which signs the user out of every live session; separately, it deletes all of their MCP tokens and revokes their OAuth tokens.
There is no **"Force password change on next login"** option in the Admin Panel. That prompt is raised only for the admin account TREK seeds on first boot and for accounts restored with the `reset-admin.js` recovery script (see [Troubleshooting](Troubleshooting)). An admin-set password neither raises the flag nor clears it, so a user who already carries it is still asked to choose their own password at the next sign-in.
See [Admin-Users-and-Invites](Admin-Users-and-Invites) for step-by-step instructions.
## Password requirements
When choosing a new password (whether via the reset flow, the forced-change prompt, or the normal **Settings → Security** page) the password must:
When choosing a new password (whether via the reset flow, the forced-change prompt, or the **Change Password** form in **Settings → Account**) the password must:
- Be at least **8 characters** long
- Contain at least one **uppercase letter**
+4 -4
View File
@@ -2,7 +2,7 @@
TREK can browse your personal photo library on Immich or Synology Photos and attach selected photos to trips. TREK never copies the original files — it stores only a reference (provider name + asset ID) and proxies all image streams through its own server, so your provider credentials are never sent to the browser.
> **Admin:** Enable at least one photo provider (Immich or Synology Photos) in **Admin → Addons** — photo provider toggles appear as sub-items under the **Journey** addon. Once a provider is on, a Photo Providers section appears in each user's **Settings → Integrations**. If your provider runs on a local or private network, the server must be configured to allow internal network access. See [Admin-Addons](Admin-Addons) and [Internal-Network-Access](Internal-Network-Access).
> **Admin:** Enable at least one photo provider (Immich or Synology Photos) in **Admin → Addons** — photo provider toggles appear as sub-items under the **Journey** addon. Once a provider is on, its settings card appears in each user's **Settings → Integrations**. If your provider runs on a local or private network, the server must be configured to allow internal network access. See [Admin-Addons](Admin-Addons) and [Internal-Network-Access](Internal-Network-Access).
---
@@ -19,7 +19,7 @@ Both providers can be active at the same time.
## Configuring a provider
Go to **Settings → Integrations → Photo Providers**. Each enabled provider shows its own settings section.
Go to **Settings → Integrations**. Each enabled provider gets its own settings card there, titled with the provider's name — **Immich** and/or **Synology Photos**.
<!-- TODO: screenshot: Photo Providers section in Settings > Integrations -->
@@ -56,8 +56,8 @@ TREK never modifies or deletes anything in Immich, so no `update`, `delete`, or
| Server URL | Yes | Full URL including the Photos app path, e.g. `https://your-nas:5001/photo` |
| Username | Yes | Synology account username |
| Password | Yes | Stored encrypted; leave blank to keep the existing password |
| OTP code | No | One-time password for 2FA; only needed on first connection or when re-authenticating |
| Skip SSL verification | No | Checkbox; disable TLS certificate validation for self-signed certificates |
| MFA code (if enabled) | No | One-time password for 2FA; only needed on first connection or when re-authenticating |
| Skip SSL certificate verification | No | Checkbox; disable TLS certificate validation for self-signed certificates |
#### Required DSM account permissions
+5 -5
View File
@@ -17,7 +17,7 @@ Type in the search box at the top of the form. After 2 or more characters, with
### With a Google Maps API key
> **Admin:** A Google Maps API key is configured in [User-Settings](User-Settings).
> **Admin:** The Google Maps API key is instance-wide, set in **Admin → Settings → API Keys**. It is stored encrypted at rest and used for every member of the instance; on managed instances the operator supplies it and the panel is hidden.
When a key is present, the autocomplete uses the Google Places API, which can return ratings, opening hours, photos, and phone numbers from Google's database.
@@ -42,7 +42,7 @@ Every picture is credited under its thumbnail with its author, its licence and a
Wikipedia and OpenStreetMap are always used and need no configuration:
- Pictures from **Wikimedia Commons**, found by coordinate.
- Pictures from **Wikimedia Commons**, resolved from the place's own tags first — the Wikidata image, the lead image of its Wikipedia article, its Commons category — and falling back to a search by coordinate only when those turn up too few.
- Descriptions from the OpenStreetMap `description` tag, or from the Wikipedia article the place is tagged with. TREK resolves the article from that tag rather than guessing it from the name, so an ambiguous name never pulls in the wrong article. A place with no such tag simply gets no description.
With a Google Maps API key, and only if the matching admin switches are on, up to three **Google Places** photos and Google's editorial summary are added on top.
@@ -59,7 +59,7 @@ Paste a `maps.app.goo.gl/…`, `goo.gl/maps/…`, or `maps.google.*/…` URL dir
## Entering coordinates manually
Type or paste a `lat, lng` pair (e.g. `48.8566, 2.3522`) into the **Latitude** field. TREK detects the comma-separated pair and fills both coordinate fields at once.
**Paste** a `lat, lng` pair (e.g. `48.8566, 2.3522`) into the **Latitude** field — comma, semicolon or space separated. TREK detects the pair and fills both coordinate fields at once. This works on paste only: the coordinate fields accept digits, a decimal point and a leading minus, so typing a pair by hand drops the separator and leaves a single invalid number (`48.85662.3522`) behind. Type the two values into their own fields instead.
## Place fields
@@ -75,7 +75,7 @@ Type or paste a `lat, lng` pair (e.g. `48.8566, 2.3522`) into the **Latitude** f
| Category | Pick an existing category or type a new name to create one inline (default color `#6366f1`, icon `MapPin`) |
| Start time / End time | Shown only when editing an existing place |
| Website | URL |
| File attachments | Images or PDFs — click the Paperclip icon or paste from the clipboard |
| File attachments | Click the Paperclip icon to attach a file, or paste an image or PDF from the clipboard. The Paperclip takes anything on the instance's **Allowed File Types** list — by default jpg, jpeg, png, gif, webp, heic, pdf, doc, docx, xls, xlsx, txt, csv, pkpass, pkpasses, md and markdown — plus video, which is exempt from that list. See [Documents-and-Files](Documents-and-Files) |
Two inline warnings are shown when editing times: one if the end time is set to a value before or equal to the start time, and one if the times overlap with another place already assigned to the same day.
@@ -101,6 +101,6 @@ Imported tracks each get their own line colour so multiple routes stay apart on
Importing the same list again does not duplicate what is already in the trip. A place is recognised by the provider id it was imported with (Google place id, Google feature id, or OSM id) before its name or its coordinates are considered, so renaming a place in TREK — or moving its pin — does not make it come back as a second copy on the next import.
> **Admin:** Google Maps API key is set in [User-Settings](User-Settings). Without it, OSM search is used automatically.
> **Admin:** The Google Maps API key is set instance-wide in **Admin → Settings → API Keys**. Without it, OSM search is used automatically.
**See also:** [Day-Plans-and-Notes](Day-Plans-and-Notes) · [Map-Features](Map-Features) · [Tags-and-Categories](Tags-and-Categories)
+8 -7
View File
@@ -47,7 +47,8 @@ Both are membership-checked against the current user — same gate as `ctx.trips
const place = await ctx.places.create(tripId, { name: 'Teamlab', lat: 35.62, lng: 139.78 })
const day = await ctx.days.create(tripId, { date: '2027-04-02', notes: 'Odaiba' })
await ctx.itinerary.assign(tripId, day.id, place.id, 'buy tickets first')
// days.create accepts { date?, notes?, position? }; set a day title later with ctx.days.update(tripId, day.id, { title: 'Odaiba' }).
// days.create reads { date?, notes? } and always appends at the end — a position is honoured on the REST route only, not on the plugin path.
// Set a day title later with ctx.days.update(tripId, day.id, { title: 'Odaiba' }).
```
Updates and deletes mirror the REST app exactly (`ctx.places.update/delete`,
@@ -257,8 +258,8 @@ whole point: you can be handed someone's push token without being handed their t
Targeting a **self-hosted** Gotify? Your manifest can't name the user's hostname, so add
`"operatorEgress": true` and let the admin add the real host after install
(Admin → Plugins → Allowed hosts). See
[Plugin-Development → Operator-supplied egress hosts](Plugin-Development.md#operator-supplied-egress-hosts-operatoregress),
plus [Notification channels](Plugin-Development.md#notification-channels) for the event list, the
[Plugin-Development → Operator-supplied egress hosts](Plugin-Development#operator-supplied-egress-hosts-operatoregress),
plus [Notification channels](Plugin-Development#notification-channels) for the event list, the
"configured" rule, and the `TREK_PLUGIN_ALLOW_PRIVATE_EGRESS` flag you need if the service runs
on your own LAN.
@@ -377,7 +378,7 @@ module.exports = {
}
```
Both jobs and scheduled tasks run with **no acting user** (trip reads are refused; only your own db + declared egress). `ctx.scheduler.set` is an upsert by name and survives restarts. Caps: ≤100 tasks/plugin, ≤128-char name, ≤8 KB payload, recurring interval ≥60s, ≤1 year out.
Both jobs and scheduled tasks run with **no acting user** (trip reads are refused; only your own db + declared egress). `at`, `in` and `every` upsert by name — scheduling the same name again replaces the pending task — and survive restarts. Caps: ≤100 tasks/plugin, ≤128-char name, ≤8 KB payload, recurring interval ≥60s, ≤1 year out.
---
@@ -387,7 +388,7 @@ Seven declarative hooks let a plugin push data into TREK's own screens — the h
```js
hooks: {
// hook:table-contributor — cells/buttons on a row. view ∈ reservations|places|day|costs|packing|files
// hook:table-contributor — cells/buttons on a row. view ∈ reservations|transports|places|day|costs|packing|files|todos
tableContributor: { async getContributions(view, tripId, ctx) {
return [{ kind: 'column', entityId: 42, id: 'crowd', label: 'Crowd', value: 'Quiet', tone: 'success' }]
} },
@@ -612,11 +613,11 @@ await ctx.accommodations.create(tripId, { place_id, start_day_id, end_day_id, ch
await ctx.packing.create(tripId, { name: 'Passport', visibility: 'personal' })
await ctx.todos.create(tripId, { name: 'Buy JR pass', due_date: '2027-04-01' })
// Costs (budget) — needs 'budget_edit' + the Costs addon
await ctx.costs.create(tripId, { title: 'Hotel', amount: 120, currency: 'EUR' })
await ctx.costs.create(tripId, { name: 'Hotel', total_price: 120, currency: 'EUR' })
// Collab notes/polls/chat — needs 'collab_edit' + the Collab addon
await ctx.collab.createNote(tripId, { title: 'Meeting point' })
// Day notes — needs 'day_edit'
await ctx.daynotes.create(tripId, dayId, { content: 'Rainy — swap plans' })
await ctx.daynotes.create(tripId, dayId, { text: 'Rainy — swap plans' })
// Tags (the acting user's own)
const tag = await ctx.tags.create({ name: 'foodie', color: '#4F46E5' })
```
+75 -19
View File
@@ -237,14 +237,14 @@ module.exports = definePlugin({
The routes and job ids you declare here are the **authoritative** ones: the host
reads them off your loaded definition (a route's array index is its internal id).
The `routes` block the scaffold writes into `trek-plugin.json` is only a
declaration for readers — the manifest parser does not consume it.
The scaffold writes no `routes` block into `trek-plugin.json`, and the manifest
parser would ignore one if you added it.
### The `ctx` object
| Area | Methods | Requires |
|---|---|---|
| `ctx.db` | `query(sql, …args)` / `exec(sql, …args)` / `migrate(id, sql)` / `tx(ops)` against your **own** SQLite file. `tx([{sql, args?}, …])` runs up to 100 statements in one transaction (all commit or all roll back; reads see the batch's own earlier writes) → `{ results: [{changes?}|{rows?}, …] }` | `db:own` |
| `ctx.db` | `query(sql, …args)` / `exec(sql, …args)` / `migrate(id, sql)` / `tx(ops)` against your **own** SQLite file. `tx([{sql, args?}, …])` runs up to 100 statements in one transaction (all commit or all roll back; reads see the batch's own earlier writes) → `{ results: [{changes?}\|{rows?}, …] }`. Your file is capped at **256 MB** (a write past it fails `SQLITE_FULL`, contained to your plugin) and a single result set at **100,000 rows** — page your reads instead of materialising a cartesian product | `db:own` |
| `ctx.trips` | `getById` / `getPlaces` / `getReservations` / `getDays` / `getAccommodations` / `listMine()` — enumerate every trip the acting user can access (membership-checked). `getDays` includes each day's `assignments` + `notes_items`; `getReservations` includes `endpoints` + `day_positions` | `db:read:trips` |
| `ctx.trips.update(tripId, fields)` | update trip fields (title/dates/currency/reminder_days/…) | `db:write:trips` |
| `ctx.trips.create(input)` | create a **new trip owned by the acting user** (importers) — `title` required, plus `description?`/`start_date?`/`end_date?`/`currency?`/`reminder_days?`/`day_count?` | `db:create:trips` (+ `trip_create`) |
@@ -333,15 +333,56 @@ validated against TREK's budget schema, and a successful create broadcasts the s
Routes are authenticated by default (`req.user` is the logged-in user). Set
`auth: false` for OAuth callbacks or webhooks that can't carry a session.
The proxy forwards `{ method, path, query, body, headers, user }`. **`req.headers`
is populated ONLY on `auth: false` routes** (an authenticated route gets `{}`) and
only an explicit, credential-free **allowlist** — the common provider signature +
event headers (`stripe-signature`, `x-hub-signature-256`, `svix-signature`,
`x-gitlab-event`, `content-type`, `user-agent`, …). **`Cookie`, `Authorization`,
`X-Socket-Id` and every session/forwarded-auth header are stripped** and never reach
your code, so a forwarded header can't leak a TREK session. To trust a webhook,
verify the provider's signature over the raw body against a secret you hold in
`ctx.config` (admin-set instance setting) or `ctx.settings` (per-user).
The proxy forwards `{ method, path, query, body, rawBodyBase64, headers, user }`.
**`req.headers` is populated ONLY on `auth: false` routes** (an authenticated route
gets `{}`) and only an explicit, credential-free **allowlist** — the common provider
signature + event headers (`stripe-signature`, `x-hub-signature-256`,
`svix-signature`, `x-gitlab-event`, `content-type`, `user-agent`, …). **`Cookie`,
`Authorization`, `X-Socket-Id` and every session/forwarded-auth header are stripped**
and never reach your code, so a forwarded header can't leak a TREK session.
**`req.rawBodyBase64` is webhook-only in the same way** — the exact request bytes,
base64-encoded, on `auth: false` routes, and absent on an authenticated route, which
never needs them. To trust a webhook, verify the provider's signature over *those*
bytes against a secret you hold in `ctx.config` (admin-set instance setting) or
`ctx.settings` (per-user). Never re-serialize `req.body` for the check:
`JSON.stringify` won't reproduce the key order, whitespace and unicode escaping the
sender signed, so the HMAC won't match.
The snippet below handles two things, and yours has to as well. TREK only keeps the
raw bytes when a body parser ran, i.e. for `application/json` and
`application/x-www-form-urlencoded` — a webhook posted as `text/plain` or a GET
callback with no body arrives with `rawBodyBase64: null`, so **fail closed** instead
of running the HMAC over an empty buffer, which is a constant anyone can forge. And
the request body ceiling is 100 kB; a larger payload is rejected before your route
runs.
```js
const crypto = require('node:crypto')
if (typeof req.rawBodyBase64 !== 'string') return { status: 400, body: { error: 'no raw body' } }
const raw = Buffer.from(req.rawBodyBase64, 'base64')
const mac = crypto.createHmac('sha256', ctx.config.webhookSecret).update(raw).digest()
const sent = Buffer.from(String(req.headers['x-hub-signature-256'] || '').replace(/^sha256=/, ''), 'hex')
if (sent.length !== mac.length || !crypto.timingSafeEqual(mac, sent)) return { status: 401, body: { error: 'bad signature' } }
```
### Runtime limits
The host enforces these on every plugin. They are generous for real work and only
bite a runaway one, but they are exactly what a plugin that loops over `ctx.*` or
stores blobs in `ctx.db` runs into, so build against them.
| Area | Limit |
|---|---|
| every `ctx.*` call | burst 60, sustained 20/s, 16 in-flight per plugin; a throttled call is refused with `HOST_ERROR: rate limit exceeded — slow down ctx.* calls`. Tunable with `TREK_PLUGIN_RPC_BURST` / `TREK_PLUGIN_RPC_PER_SEC` / `TREK_PLUGIN_RPC_INFLIGHT` (see [Environment-Variables](Environment-Variables#plugins)) |
| `ctx.db` | 256 MB per plugin, one result set capped at 100,000 rows |
| event subscriptions | 200 events buffered per plugin while it restarts, dropped unreplayed after 15 minutes |
| the plugin process | 300 MB RSS (`TREK_PLUGIN_MAX_RSS_MB`) — the child is killed past it; auto-disabled with status `error` after 5 crashes inside a 5-minute window |
The daily `ctx.ai` / `ctx.notify` budgets and the `ctx.meta` quotas are in
[Testing without a running TREK](#testing-without-a-running-trek), because the mock
host enforces those too.
## Writing the client (page / widget)
@@ -819,9 +860,19 @@ also being given the right to read their trips as them.
Notes:
- **`title`** names the column in the notification preferences matrix (defaults to your
plugin's name). **`events`** may *narrow* which events the channel carries; the default is
every non-admin event. Admin-scoped events (`version_available`) are never deliverable to a
plugin channel — those go over the admin's own credentials.
plugin's name). **`events`** may *narrow* which events the channel carries; omit it and
the channel carries all ten deliverable events: `trip_invite`, `booking_change`,
`trip_reminder`, `todo_due`, `vacay_invite`, `collection_invite`, `photos_shared`,
`collab_message`, `packing_tagged`, `plugin_notification`. The SDK exports the same list
as `CHANNEL_EVENTS`, and both `trek-plugin validate` and the installer refuse anything
outside it with `capabilities.notificationChannel.events: "x" is not a plugin-deliverable
event`.
The four events TREK sends that a plugin channel never carries are the admin-scoped
`version_available` and `replica_failure` (those go over the admin's own credentials),
the in-app-only `synology_session_cleared`, and `vacay_share`. Don't read the set off the
table in [Notifications](Notifications) — that one lists what a user can toggle, not what
a channel can carry.
- **Configured-ness is inferred, not asked.** A user's column is "not configured" until every
`required`, `scope:'user'` field has a value. A plugin with no required user fields counts as
configured for everyone (an instance-wide channel, e.g. a shared workspace webhook).
@@ -1095,10 +1146,14 @@ import { createMockHost } from 'trek-plugin-sdk/testing'
const { ctx, broadcasts } = createMockHost({
grants: ['db:read:trips'],
trips: { 1: { members: [42], data: { id: 1, name: 'Japan' } } },
actingUserId: 42, // the user every read is checked against
trips: {
1: { members: [42], data: { id: 1, name: 'Japan' } },
2: { members: [99], data: { id: 2, name: 'Peru' } },
},
})
await ctx.trips.getById(1, 42) // ok — member
await expect(ctx.trips.getById(1, 99)).rejects… // RESOURCE_FORBIDDEN
await ctx.trips.getById(1) // ok — member
await expect(ctx.trips.getById(2)).rejects… // RESOURCE_FORBIDDEN
await expect(ctx.db.query('SELECT 1')).rejects… // PERMISSION_DENIED (no db:own)
```
@@ -1198,6 +1253,7 @@ what your manifest declares.
| `description` | string | one-line summary for the store. |
| `icon` | string | lucide-react icon name (default `Blocks`); used for the page nav entry. |
| `homepage` | string | project URL. |
| `tags` | string[] | store keywords, copied verbatim onto the registry entry. Each must be a lowercase slug of 2–24 chars (`[a-z0-9-]`), at most 8 — `validate` rejects a capital or a space, because the registry's schema does. |
| `license` | string | shown in the store detail (read from the manifest, not enforced). |
| `nativeModules` | boolean | must be `false`/absent — `true` is rejected. |
| `permissions` | string[] | see below. |
@@ -1206,7 +1262,7 @@ what your manifest declares.
| `capabilities.tripPage` | object | `{ replaces?, position? }` for `trip-page` plugins — `replaces` names core planner tabs to hide while active (`transports`, `buchungen`, `listen`, `finanzplan`, `dateien`, `collab`; never `plan`), `position` is the tab's 0-based index in the bar (0–50; omitted = appended). |
| `actions` | array | Buttons on the plugin's own settings page — `{ key, label, hint?, danger? }` (max 8). Implement each as `actions[key](ctx)` on the definition. **User-initiated**, so `ctx.settings.get()` returns the clicking user's value. See [Settings-page actions](#settings-page-actions). |
| `operatorEgress` | boolean | The plugin talks to a **self-hosted** service whose hostname only the operator knows. The admin adds the real hosts after install (Admin → Plugins → Allowed hosts) and the runtime unions them into the egress allow-list. Requires an `http:outbound` permission, and is the only way to declare one with an empty `egress[]`. See [Operator-supplied egress hosts](#operator-supplied-egress-hosts-operatoregress). |
| `capabilities.notificationChannel` | object | `{ title?, events? }` for a plugin implementing the `notificationChannel` hook — `title` names the column in the notification preferences matrix (default: the plugin's `name`), `events` **narrows** which events the channel carries (default: every non-admin event; admin-scoped events are never deliverable). Requires the `hook:notification-channel` permission. See [Notification channels](#notification-channels). |
| `capabilities.notificationChannel` | object | `{ title?, events? }` for a plugin implementing the `notificationChannel` hook — `title` names the column in the notification preferences matrix (default: the plugin's `name`), `events` **narrows** which events the channel carries (default: all ten plugin-deliverable events; `events` may only narrow that set). Requires the `hook:notification-channel` permission. See [Notification channels](#notification-channels). |
| `capabilities.routeProfiles` | array | up to 3 `{ id, label, icon? }` entries for a plugin implementing the `routeProvider` hook — each becomes a selectable mode in the planner's route toggle (next to Driving/Walking). `id` is lowercase `[a-z][a-z0-9-]` (max 24 chars) and is what `getRoute` receives as `request.profile`; `label` (≤40 chars) is shown to the user. Requires the `hook:route-provider` permission. |
| `capabilities.provides` | string[] | function names this plugin exposes to its dependents via `ctx.plugins.call` (see [Talking to other plugins](#talking-to-other-plugins)). |
| `capabilities.emits` | string[] | event names this plugin publishes to its dependents via `ctx.events.emit`. |
+2 -2
View File
@@ -10,7 +10,7 @@ ungranted capability is physically unreachable**, not just disallowed. See
| Permission | Grants | Notes |
|---|---|---|
| `db:own` | Read/write the plugin's **own** SQLite file via `ctx.db` — `db.query`, `db.exec`, **and `db.migrate`** | A separate file per plugin — never `trek.db`. `db.migrate` runs a keyed, idempotent migration (schema/table creation, e.g. `CREATE TABLE`) once per id. `ATTACH`/`DETACH`/`VACUUM`/`PRAGMA` are refused. |
| `db:own` | Read/write the plugin's **own** SQLite file via `ctx.db` — `db.query`, `db.exec`, **and `db.migrate`** | A separate `plugin.db` per plugin under the plugin's data dir — never TREK's own `travel.db`. `db.migrate` runs a keyed, idempotent migration (schema/table creation, e.g. `CREATE TABLE`) once per id. `ATTACH`/`DETACH`/`VACUUM`/`PRAGMA` are refused. |
| `db:read:trips` | Read-only trip data via `ctx.trips` (`getById`, `getPlaces`, `getReservations`, `getDays`, `getAccommodations`, `listMine`, `members`) | Every call is **membership-checked** against the acting user — a plugin can't read a trip that user can't see. `getDays` returns each day with its `assignments` + `notes_items`; `getReservations` rows carry `endpoints` + `day_positions` like the REST list. `members` returns the roster (id + display fields only). |
| `db:read:users` | Read-only public profile via `ctx.users.getById` | Returns id, username, display name, avatar only — **never** password hashes, tokens, or secrets. |
| `db:read:packing` | Read-only packing items of a trip via `ctx.packing.list(tripId)` | Membership-checked, and scoped to the acting user's visibility — a plugin never sees another member's private packing items. |
@@ -65,7 +65,7 @@ ungranted capability is physically unreachable**, not just disallowed. See
| `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`. |
| `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, places, day, costs, packing and files views; an action opens your sandboxed frame or calls one of your routes. Also exposed at `GET /api/view-contributions/:view/:tripId`. |
| `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`. |
| `hook:atlas-layer-provider` | Draw country **tint layers** over the Atlas world map via the `hooks.atlasLayerProvider` provider hook | Implement `AtlasLayerProvider` in `hooks` — returns `{id, name?, countries: [{code, tone?, label?}]}[]` for the **acting user** (the hook takes no target parameter, so a plugin can't ask for anyone else's map). Codes must be ISO-3166 alpha-2 (uppercase-coerced, anything else dropped), tone is enum-whitelisted, counts capped (≤3 layers, ≤300 countries each). **Declarative only** — plugin JS never runs on the map canvas. Exposed at `GET /api/atlas-layers`. |
| `hook:journal-entry-provider` | Contribute extra **rows** under a journal entry via the `hooks.journalEntryProvider` provider hook | Implement `JournalEntryProvider` in `hooks` — returns `{label, value?, url?}[]` per entry, rendered under the entry card. The entry's journey is access-checked against the acting user (owner/contributor, like the journal detail routes) and the Journey addon must be on. The host caps the row count (≤12) + lengths (label 60, value 200) and allowlists the url (http/https/mailto); a failing provider is skipped. Exposed at `GET /api/journal-entry-rows/:entryId`. |
+10 -8
View File
@@ -106,11 +106,13 @@ Between them they now catch, **offline**, nearly everything the registry rejects
63-entry list the host enforces, so a typo'd permission fails here instead of at
install on every instance
Only **four** gates genuinely need the network, and they all need the release to exist:
that the tag resolves to the commit the entry pins, that the released artifact downloads
and hashes to the pinned sha256 (and verifies against your key), that your plugin id
isn't bound to another GitHub owner, and that an update doesn't drop or rotate a signing
key you already published under. Those run in `preflight` — step 4 of `publish`.
Only **six** gates genuinely need the network, because none of them can be answered from
your working tree: that the tag resolves to the commit the entry pins, that the manifest at
that commit matches the entry, that the README at that commit still passes the quality gate,
that the released artifact downloads and hashes to the pinned sha256 (and verifies against
your key), that your plugin id isn't bound to another GitHub owner, and that an update
doesn't drop or rotate a signing key you already published under. Those run in `preflight`
— step 4 of `publish`.
### Capture the screenshot
@@ -211,9 +213,9 @@ release, so you catch what CI would reject without a review round-trip:
npx trek-plugin-sdk preflight --repo you/trek-plugin-flight-tracker --tag v1.0.0
```
`preflight` runs everything `validate` runs, plus the four gates that genuinely need
the network: the tag resolves to the pinned `commitSha`; the manifest **and the
README** at that commit match the entry and pass the quality gate; the released
`preflight` runs everything `validate` runs, plus the six gates that genuinely need
the network: the tag resolves to the pinned `commitSha`; the **manifest** at that commit
matches the entry; the **README** at that commit passes the quality gate; the released
artifact downloads, hashes to the pinned **sha256**, carries **no native binaries** and
verifies against your key; your plugin id is not bound to a different GitHub owner; and
an update does not drop or rotate a signing key you already published under.
+14 -8
View File
@@ -29,8 +29,9 @@ An `integration` plugin can add a whole new **notification channel** — Gotify,
Telegram, anything that takes a message — alongside TREK's built-in email, webhook and
ntfy.
Once you install and enable such a plugin, switch its channel on in
**Admin → Notifications**. It then appears as a new column in every user's
Once you install and activate such a plugin, its channel is live — there is no separate
switch for it in **Admin → Notifications**, which only turns the built-in email, webhook
and ntfy channels on. It appears right away as a new column in every user's
**Settings → Notifications** matrix, and each user supplies their own credentials on the
plugin's own settings page and picks per-event what they want pushed — exactly like a
built-in channel.
@@ -95,9 +96,10 @@ code:
- It has **no** access to `JWT_SECRET`, the database connection, or any TREK
secret — those are simply not reachable by its process.
- It **cannot** open `trek.db`, write files, spawn child processes, use worker
threads, or load native addons. Its own data lives in a separate SQLite file it
reaches only through TREK.
- It **cannot** open TREK's own database (`server/data/travel.db`, or
`/app/data/travel.db` in the container), write files, spawn child processes, use
worker threads, or load native addons. Its own data lives in a separate SQLite file
it reaches only through TREK.
- It talks to TREK exclusively over an internal RPC channel, and TREK only
answers the capabilities the plugin's manifest **declares and you approve**.
An ungranted call is refused, not merely ignored.
@@ -123,9 +125,11 @@ A single panel with a segmented **Installed / Discover** switch at the top left,
plus a toolbar:
- **Search** — filters the current list by name/description (and author, in Discover).
- **Type** filter — All / Widget / Integration / Page.
- **Type** filter — All types / Widget / Integration / Page / Trip page.
- **Status** filter (Installed view only) — All / Active / Off / Update available / Error.
- **Sort** — Name / Recently updated / Updates first.
- **Sort** — Name / Recently updated, plus **Updates first** in Installed and **Most
downloads** in Discover. The view-specific option falls back to Name when you switch
views.
- **Upload** — sideload a plugin from a `.zip`/`.tar.gz` (see [Installing](#installing-a-plugin)).
- **Rescan** — rediscovers the on-disk plugins directory **and** force-pulls the
remote registry, bypassing the 30-minute server cache and GitHub's CDN so a
@@ -222,7 +226,9 @@ The **⋯** menu on each row:
- **Allowed hosts** — add the hosts a plugin may reach, for a plugin that talks to a
service only *you* can name. See [Allowed hosts](#allowed-hosts) below.
- **Source repository** — opens the plugin's GitHub repo (registry installs only).
- **Delete** — uninstalls: removes the code and lets you keep or delete its data.
- **Report an issue** — opens that repo's issue tracker (registry installs only).
- **Delete** — uninstalls, after a confirmation: it stops the plugin, removes its code,
**and deletes all of its data**. This cannot be undone.
## Allowed hosts
+16 -6
View File
@@ -18,31 +18,41 @@ The share URL takes the form:
Copy this URL and send it to anyone you want to share the trip with. No TREK account is required to view it.
### How long a link lives
A share link is valid for **90 days**, counted from the last time it was saved. Creating the link writes a 90-day expiry, and flipping one of the unlocked permission toggles re-saves the link and writes a new one, so a link you actively manage never lapses under you.
Once the 90 days are up, visitors get **Link expired or invalid**. The share section in the Members & Share modal does not show this — it keeps displaying the URL and the **Delete link** button, because the owner-side lookup ignores the expiry. There is no expired badge to warn you.
To revive a lapsed link, flip one of the unlocked toggles — **Bookings**, **Packing**, **Costs** or **Chat** — and flip it back if you did not mean to change anything. **Map & Plan** is locked, so clicking it sends no request and will not revive the link. Flipping an unlocked toggle re-saves the **same** token for another 90 days and the old URL starts working again. If you want a genuinely different URL, because the old one leaked for instance, use **Delete link** and then **Create link**.
Links created before TREK added the expiry carry no expiry at all and work indefinitely. The first time you change one of their toggles, that save puts them on the 90-day clock like every other link.
## Permission toggles
When creating or updating a share link you choose what the recipient can see. The available flags are:
| Toggle | Default | What it shows |
|--------|---------|---------------|
| **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. |
| **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 |
| **Budget** (`share_budget`) | Off | The Budget tab with a total summary and line items grouped by category |
| **Collab** (`share_collab`) | Off | A read-only Chat tab showing messages in chronological order |
| **Costs** (`share_budget`) | Off | The Costs tab with a total summary and line items grouped by category |
| **Chat** (`share_collab`) | Off | A read-only Chat tab showing messages in chronological order |
Disabled toggles hide the corresponding tab from the public viewer entirely. Permission changes take effect immediately — you do not need to recreate the link.
### Which currency guests see
A public viewer has no account, so there is no "their" display currency to use. The Budget tab is rendered in **the sharer's display currency, falling back to the trip's own currency** — in other words, a guest sees the money the way the person who shared the trip sees it. If the sharer leaves their display currency on **Trip currency** (the default), guests read the trip in the trip's own currency. See [Currencies](Currencies).
A public viewer has no account, so there is no "their" display currency to use. The Costs tab is rendered in **the sharer's display currency, falling back to the trip's own currency** — in other words, a guest sees the money the way the person who shared the trip sees it. If the sharer leaves their display currency on **Trip currency** (the default), guests read the trip in the trip's own currency. See [Currencies](Currencies).
## What the public viewer shows
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 and shows an interactive map, a collapsible day-by-day itinerary (with places, notes, and transport inline when Bookings is enabled), and accommodation badges per day.
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 Collab tab (when enabled via `share_collab`) shows chat messages grouped by date with sender avatars. Viewers cannot send messages.
The Chat tab (when enabled via `share_collab`) shows chat messages grouped by date with sender avatars. Viewers cannot send messages.
## Revoking a share link
+3 -3
View File
@@ -14,10 +14,10 @@ Get TREK running in under five minutes with a single Docker command.
Generate an encryption key and start the container in one step:
```bash
ENCRYPTION_KEY=$(openssl rand -hex 32) docker run -d \
docker run -d \
--name trek \
-p 3000:3000 \
-e ENCRYPTION_KEY=$ENCRYPTION_KEY \
-e ENCRYPTION_KEY="$(openssl rand -hex 32)" \
-v ./data:/app/data \
-v ./uploads:/app/uploads \
--restart unless-stopped \
@@ -35,7 +35,7 @@ ENCRYPTION_KEY=$(openssl rand -hex 32) docker run -d \
| `-v ./uploads:/app/uploads` | Persist uploaded files |
| `--restart unless-stopped` | Auto-restart on reboot |
**Why the encryption key?** TREK encrypts stored secrets (API keys, MFA seeds, OIDC credentials) using this key. If you skip it, TREK auto-generates one and saves it to `./data/.encryption_key`. Setting it explicitly means you control the key and can back it up separately.
**Why the encryption key?** TREK encrypts stored secrets (API keys, MFA seeds, OIDC credentials) using this key. If you skip it, TREK auto-generates one and saves it to `./data/.encryption_key`. Setting it explicitly means you control the key and can back it up separately. The key generated inline above never gets printed, but TREK writes it to `./data/.encryption_key` on first start, so `cat ./data/.encryption_key` gets it back when you want a copy elsewhere.
Generate a standalone key at any time:
+4 -1
View File
@@ -14,15 +14,18 @@ All changes to a trip — places, day plans, reservations, budget entries, and p
|-----------|-------|
| Path | `/ws` |
| Scope | One room per trip |
| Rate limit | 30 messages per 10-second window |
| Rate limit | 30 messages per 10-second window, plus a looser ceiling of 200 frames per 10 seconds that every frame counts against before it is parsed. Live cursor frames (`book:cursor`) are exempt from the tighter limit, not from the looser one |
| Max payload | 64 KB per message |
| Heartbeat | Ping every 30 seconds; sockets that miss a pong are terminated |
| Origin check | Only enforced when `ALLOWED_ORIGINS` is set: an upgrade whose `Origin` header is not an exact match for a list entry is refused with HTTP **403** during the handshake. An upgrade that sends no `Origin` header at all is always admitted |
**Authentication** uses a short-lived ephemeral token passed as a query parameter on connect. If authentication fails, the server closes the connection with one of these codes:
- **4001** — missing, invalid, or expired token; user not found
- **4403** — site-wide MFA is required but the account does not have MFA enabled
An origin rejection happens before the socket exists, so it produces no close code at all — the browser just reports a failed connection. And because clients that send no `Origin` header are exempt, curl and other CLI clients keep connecting while every browser fails, which makes it look like a client bug. If real-time sync stops working behind a reverse proxy, check that `ALLOWED_ORIGINS` lists the exact scheme, host, and port the browser uses: the comparison is a plain string match, so `https://trek.example.com` matches neither `http://trek.example.com` nor `https://trek.example.com:443`. See [Environment-Variables](Environment-Variables) and [Reverse-Proxy](Reverse-Proxy).
## The Collab addon
The Collab addon (`collab`) must be enabled by an admin before the panel is visible inside a trip. Once enabled, it provides four sub-features that can each be toggled independently:
+38 -25
View File
@@ -4,28 +4,34 @@ Track all your trip bookings — hotels, restaurants, events, tours, and more
## Where to find it
Open your trip in the planner and select the **Reservations** tab. The panel lists all bookings grouped by status, with a type filter bar at the top.
Open your trip in the planner and select the **Bookings** tab. The panel lists your hotel, restaurant, event, tour, parking and other bookings grouped by status, with a type filter bar at the top. Transport bookings live on their own **Transports** tab — see [Transport-Flights-Trains-Cars](Transport-Flights-Trains-Cars).
![Reservations panel](assets/Bookings.png)
## Reservation types
TREK supports ten reservation types:
TREK supports sixteen reservation types, split across the two tabs:
| Type | How to create |
|------|--------------|
| Flight | [Transport modal](Transport-Flights-Trains-Cars) |
| Train | [Transport modal](Transport-Flights-Trains-Cars) |
| Bus | [Transport modal](Transport-Flights-Trains-Cars) |
| Car | [Transport modal](Transport-Flights-Trains-Cars) |
| Taxi | [Transport modal](Transport-Flights-Trains-Cars) |
| Bicycle | [Transport modal](Transport-Flights-Trains-Cars) |
| Cruise | [Transport modal](Transport-Flights-Trains-Cars) |
| Hotel | Add button in Reservations panel — see [Accommodations](Accommodations) |
| Restaurant | Add button in Reservations panel |
| Event | Add button in Reservations panel |
| Tour | Add button in Reservations panel |
| Parking | Add button in Reservations panel — a fixed-location booking (e.g. airport parking) |
| Other | Add button in Reservations panel |
| Ferry | [Transport modal](Transport-Flights-Trains-Cars) |
| Public transit | [Transport modal](Transport-Flights-Trains-Cars) |
| Other (transport) | [Transport modal](Transport-Flights-Trains-Cars) |
| Accommodation (Hotel) | Add button in Bookings panel — see [Accommodations](Accommodations) |
| Restaurant | Add button in Bookings panel |
| Event | Add button in Bookings panel |
| Tour | Add button in Bookings panel |
| Parking | Add button in Bookings panel — a fixed-location booking (e.g. airport parking) |
| Other | Add button in Bookings panel |
Transport types (Flight, Train, Car, Cruise) are created through the dedicated Transport modal, where you can enter endpoint and transit-specific fields. All other types are created directly from the Reservations panel.
The ten transport types are created through the dedicated Transport modal on the **Transports** tab, where you can enter endpoint and transit-specific fields. The other six are created directly from the Bookings panel, and only those six appear there.
## Pending and Confirmed
@@ -33,6 +39,8 @@ Reservations are grouped into two collapsible sections: **Pending** and **Confir
On desktop, a type filter bar lets you show only specific types. Filter selections are kept for the current browser session.
A traveler filter sits next to it — on desktop and mobile — once the trip has more than one member and at least one booking has travelers assigned. Click an avatar to show only that person's bookings; several can be active at once. On desktop the selection is kept for the browser session like the type filter; on mobile it resets when you leave the tab.
## Reservation card contents
Each card displays:
@@ -55,25 +63,30 @@ Each card displays:
- **Location / address** — for non-hotel, non-transport types
- **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
- **Notes**
- **Attached files** — shown as clickable download links
- **Travelers** — avatars of the members and guests assigned to this booking, shown only when at least one is assigned
## Creating a reservation
Click **Add** (or the + button) in the Reservations panel. Fill in the form:
Click **Add** (or the + button) in the Bookings panel. Fill in the form:
1. **Type** — choose Hotel, Restaurant, Event, Tour, Parking, or Other
2. **Title** — required
3. **Link to day-plan assignment** — optional; search across all days and places, grouped by day. Not available for Hotel type
4. **Start date and time** — not shown for Hotel type
5. **End date and time** — not shown for Hotel type
6. **Location / address** — not shown for Hotel type
7. **Confirmation code**
8. **Status** — Pending or Confirmed
9. **Hotel-specific fields** — shown only for Hotel type, immediately after status: hotel place, check-in day, check-out day, check-in time (window start and end), and check-out time. See [Accommodations](Accommodations)
10. **Notes**
11. **Files** — attach from your device (PDF, Word documents, text files, images) or link an existing trip file. Files added before saving are uploaded automatically after the reservation is created
12. **Costs** — shown only when the Budget addon is enabled. Instead of a price field, the form carries a **Create expense** button: it saves the booking and then opens the Costs editor for a new expense linked to it, so the expense gets a payer, a split and a date like any other. Once linked, the block shows that expense with edit and remove actions. See [Budget-Tracking](Budget-Tracking)
6. **Place / Activity** — optionally link an existing trip place to the booking. Picking one fills in the title and the location only where you left them blank. Not shown for Hotel type, which has its own hotel place picker below
7. **Location / address** — for Hotel type this field sits in the hotel-specific block instead (item 10)
8. **Confirmation code**
9. **Status** — Pending or Confirmed
10. **Hotel-specific fields** — shown only for Hotel type, immediately after status: hotel place, check-in day, check-out day, location / address, check-in time (window start and end), and check-out time. The address is pre-filled from the picked hotel place, and a hand-typed one is kept if that place has none. See [Accommodations](Accommodations)
11. **Link** — an optional booking URL, shown for every type
12. **Notes**
13. **Travelers** — assign trip members and named guests to this booking. Guests appear in the same picker as members
14. **Files** — attach from your device (PDF, Word documents, text files, images) or link an existing trip file. Files added before saving are uploaded automatically after the reservation is created
15. **Costs** — shown only when the Budget addon is enabled. Instead of a price field, the form carries a **Create expense** button: it saves the booking and then opens the Costs editor for a new expense linked to it, so the expense gets a payer, a split and a date like any other. Once linked, the block shows that expense with edit and remove actions. See [Budget-Tracking](Budget-Tracking)
<!-- TODO: screenshot: Create Reservation modal -->
@@ -95,14 +108,14 @@ Up to 5 files, 10 MB each, per import.
### How to import
1. Open the **Reservations** tab.
2. Click the **Import** (download) button in the toolbar — the button is only shown when the extractor is available on your server.
1. Open the **Bookings** tab — the same import button also sits in the **Transports** toolbar.
2. Click the **Import from file** (download) button in the toolbar — the button is only shown when the extractor is available on your server or the AI Parsing addon is enabled.
3. Drag and drop your files onto the upload area, or click to browse.
4. TREK parses each file and shows a **preview list** of the detected reservations with type, title, dates, endpoints, and confirmation number.
5. Deselect any items you do not want to import by clicking the × on their card.
6. Click **Confirm** to create the selected reservations.
4. The upload dialog closes right away and a **background widget** in the bottom-right corner shows *Parsing files…*, with a running count when you uploaded more than one file. You can keep working in TREK while it parses — the widget follows you to other pages and survives a reload.
5. When parsing finishes, click the widget's **Import** button to start the review. If nothing could be extracted, the widget says so instead and offers **Try AI parsing** on the same files when the AI Parsing addon is enabled.
6. Each detected booking opens **pre-filled in the normal booking (or transport) form**, one after the other, with the file it came from already attached. Nothing is saved until you confirm each one.
All created reservations appear immediately in the panel and are broadcast to all connected trip members in real time.
Each booking appears in the panel and is broadcast to all connected trip members in real time as you save it.
### What gets created automatically
@@ -112,7 +125,7 @@ All created reservations appear immediately in the panel and are broadcast to al
### When the button is not visible
The Import button is hidden when the `kitinerary-extractor` binary is not available. The binary ships inside the official TREK Docker image. If you run TREK from source, install the `libkitinerary-bin` package (Debian trixie / Ubuntu 25.04+) or set `KITINERARY_EXTRACTOR_PATH` to the binary's full path. See [Environment-Variables](Environment-Variables).
The **Import from file** button is hidden only when neither the `kitinerary-extractor` binary nor the [AI Parsing addon](AI-Booking-Import) is available. With the AI addon enabled and configured, import works without the binary — every file goes straight to the model. The binary ships inside the official TREK Docker image. If you run TREK from source, install the `libkitinerary-bin` package (Debian trixie / Ubuntu 25.04+) or set `KITINERARY_EXTRACTOR_PATH` to the binary's full path. See [Environment-Variables](Environment-Variables).
### Needs review flag
@@ -124,7 +137,7 @@ KDE Itinerary only recognises structured tickets. For confirmations it can't rea
## Import from AirTrail
With the **AirTrail** integration addon enabled and your instance connected under **Settings → Integrations**, the reservations toolbar shows an **AirTrail** button. It lists the flights from your AirTrail account — flights inside the trip dates come pre-selected — and imports each one as a flight reservation that stays in sync with AirTrail both ways.
With the **AirTrail** integration addon enabled and your instance connected under **Settings → Integrations**, the **Transports** toolbar shows an **AirTrail** button. It lists the flights from your AirTrail account — flights inside the trip dates come pre-selected — and imports each one as a flight reservation that stays in sync with AirTrail both ways.
### Connecting flights (layovers)
+5 -2
View File
@@ -124,15 +124,18 @@ TREK sends an SSE keep-alive comment every 25 seconds (`MCP_SSE_KEEPALIVE`) prec
## HTTPS Environment Variables
Four variables control how TREK behaves behind a proxy. They work as a group:
Five variables control how TREK behaves behind a proxy. They work as a group:
| Variable | Purpose | Default |
|---|---|---|
| `FORCE_HTTPS` | When `true`: 301-redirects HTTP→HTTPS (except `/api/health`), sends HSTS (`max-age=31536000`), adds CSP `upgrade-insecure-requests`, forces cookie `secure` flag | `false` |
| `HSTS_INCLUDE_SUBDOMAINS` | When `true`: adds the `includeSubDomains` directive to the HSTS header, extending HTTPS enforcement to all subdomains. Only effective while HSTS is active. Leave `false` if you run other services on sibling subdomains over plain HTTP. | `false` |
| `TRUST_PROXY` | Number of trusted proxy hops. Lets Express read the real client IP from `X-Forwarded-For`. Automatically set to `1` in production even if not explicitly configured. | `1` (production), off (development) |
| `COOKIE_SECURE` | Controls the `secure` flag on `trek_session`. Auto-derived as `true` when `NODE_ENV=production` or `FORCE_HTTPS=true`. Set to `false` explicitly to allow cookies over plain HTTP (e.g. LAN testing without TLS). | auto |
| `COOKIE_SECURE` | Controls the `secure` flag on `trek_session`. Auto-derived as `true` when `NODE_ENV=production`, when `FORCE_HTTPS=true`, or when the request itself arrived over TLS — Express sets `req.secure` once your proxy sends `X-Forwarded-Proto: https` and `TRUST_PROXY` is configured. Set to `false` explicitly to allow cookies over plain HTTP (e.g. LAN testing without TLS). | auto |
| `ALLOWED_ORIGINS` | Comma-separated list of allowed CORS origins (e.g. `https://trek.example.com`). In production without this set, all cross-origin requests are blocked. In development without this set, all origins are allowed. | blocked in prod, open in dev |
> **Note on HSTS:** The `max-age=31536000` header goes out whenever `FORCE_HTTPS=true` **or** `NODE_ENV=production`, and `NODE_ENV=production` is the default in the Docker image, the compose file and the Helm chart — so a standard install sends HSTS even with `FORCE_HTTPS` unset. Browsers ignore the header on a plain-HTTP response, so an instance you only ever reach over HTTP is unaffected. Once a browser has seen it over HTTPS, though, it refuses plain HTTP to that hostname for a year, so don't plan on falling back to `http://` for a hostname you have already served over HTTPS.
> **Note on `FORCE_HTTPS` and proxy headers:** The HTTPS redirect reads `X-Forwarded-Proto` directly from the incoming headers — it does not depend on Express's `trust proxy` setting. If you set `FORCE_HTTPS=true` and your reverse proxy correctly sends `X-Forwarded-Proto: https`, the redirect will work regardless of `TRUST_PROXY`. However, you still need `TRUST_PROXY` set so Express resolves the correct client IP from `X-Forwarded-For`.
If you access TREK directly on `http://<host>:3000` without a proxy, leave `FORCE_HTTPS` unset and do not set `TRUST_PROXY`.
+19 -6
View File
@@ -10,25 +10,38 @@ TREK uses **OSRM** (Open Source Routing Machine) to calculate routes between con
The route toggle in the day-plan footer offers a **Driving** and a **Walking** profile (each routed on the matching OSRM network). Installed plugins can add further profiles: a plugin with the `routeProvider` hook — for example an e-mobility plugin that plans charging stops — appears as an extra mode next to Driving/Walking. When such a profile is selected, that plugin computes the day's route: its geometry is drawn on the map, planned stops (e.g. chargers) appear as small dots on the line, and the leg connectors show the plugin's travel times plus any note it attaches ("25 min charge"). If the plugin fails or times out, TREK falls back to straight lines exactly as it does on an OSRM outage.
That toggle sets the **day's** default travel mode. It is stored on the day, so each day of a trip can differ and the choice survives a reload.
A single leg can override the day default: click the connector row between two entries and pick **Driving**, **Walking**, or any plugin profile for that leg alone, or **Use day default** to clear the override again. The same menu sits on the connectors to and from the day's accommodation and on the connector arriving after a transport booking, where it sets the mode of the leg *arriving* at that stop. Every connector shows the icon and the time of the mode its leg was actually drawn with.
When the trip has a start and an end date and you may edit the day, that menu also carries a **Public transit** entry. It opens the automated transit search (see [Transport-Flights-Trains-Cars](Transport-Flights-Trains-Cars)) already filled in with the leg's two endpoints and the departure time of the stop you are leaving.
Route segments reset at any transport reservation (flight, train, car, bus, or cruise) between two places — that leg is not driven or walked, so no ground route is drawn across it.
## Route display
- Colored line segments connect consecutive places on the map.
- At zoom level 12 or higher, time pills show the estimated walking and driving time between each pair of consecutive places.
- When at least two places are on the selected day, total distance and duration are shown in the sidebar footer.
- Between each pair of consecutive places, a slim connector row in the day plan carries that leg's travel time and distance, with an icon for the mode it was routed in.
- Plugins can attach time entries to the day plan (e.g. planned charging time at a stop, a security buffer before a flight). They appear as slim rows under the place or booking they belong to, and their minutes are added to the day's footer total as "+X min".
## Optimize route
The **Optimize** button in the sidebar footer reorders places in the current day to minimize total travel distance using a **nearest-neighbor algorithm**. It starts from the first place, then repeatedly visits the closest unvisited place by straight-line (Euclidean) distance.
The **Optimize** button in the day's route tools reorders places in the current day to minimize total travel distance. A **nearest-neighbor** pass produces a good starting order, then a **2-opt** pass untangles the crossings that pass leaves behind — both measure straight-line (Euclidean) distance.
Only unlocked places are reordered — locked places stay in their current positions.
With **Optimize route from accommodation** (Settings → General → Travel & map, on by default) the run is anchored on the day's hotel: a loop out from and back to it, or a hotel-to-hotel run on a transfer day. With the setting off, or on a day whose accommodation has no coordinates, it starts from the first place instead. A day with fewer than three places is left alone on desktop, and the mobile day sheet only offers the button from three up. The **Optimize** action in the plan screen's **Plan** mode has no such floor — two movable places with coordinates are enough, and with a hotel anchoring the run even those two can swap.
Only free places are reordered. A place keeps its slot if you locked it, or if it has a time set — a timed stop is anchored by its time. Those stops stay where they are and the reordered ones fill the gaps between them. On mobile there is no lock, so only a set time pins a stop.
The reorder can be undone immediately using the undo action that appears after it is applied.
## Export day to Google Maps
## Open the day in a map app
The **Open in Google Maps** button (icon next to Optimize) generates a `https://www.google.com/maps/dir/lat,lng/lat,lng/…` URL containing all places in order and opens it in a new tab.
Two icon buttons in the day's route tools hand the current day to an external map app, both using its places in planned order, bookended by the day's accommodation exactly the way the drawn route is.
**Open in Google Maps** builds a `https://www.google.com/maps/dir/lat,lng/lat,lng/…` URL containing all stops in order and opens it in a new tab. A day with a single stop opens as a map search on that point instead.
**Open in CoMaps** (the compass icon beside it) hands the same day to CoMaps for offline navigation and carries the day's travel mode with it — the day's own default, or the current route profile when the day has none. A day of exactly two stops goes over as a real turn-by-turn route in that mode (walking as pedestrian, cycling as bicycle, everything else as vehicle). Any other day goes over as named pins, because CoMaps' route link takes a start and a destination and nothing between them, and handing over the whole day beats quietly dropping the middle of the plan. For the full itinerary as one navigable track, use the GPX export.
Both buttons are on mobile as well, in the day sheet.
**See also:** [Day-Plans-and-Notes](Day-Plans-and-Notes) · [Map-Features](Map-Features) · [Display-Settings](Display-Settings)
+9 -8
View File
@@ -12,19 +12,20 @@ A production TREK deployment checklist. All items reference actual TREK configur
- [ ] Run TREK behind a TLS-terminating reverse proxy (nginx, Caddy, Traefik). See [Reverse-Proxy](Reverse-Proxy).
- [ ] 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 enable HSTS (`max-age=31536000`), redirect HTTP to HTTPS, and add `upgrade-insecure-requests` to the CSP. Requires `TRUST_PROXY` — omitting it causes a redirect loop.
- [ ] 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.
## Authentication
- [ ] Enable two-factor authentication for your admin account. See [Two-Factor-Authentication](Two-Factor-Authentication).
- [ ] Require MFA for all users via [Admin-Permissions](Admin-Permissions) if your use case demands it. Note: you must have MFA enabled on your own admin account before you can enforce it globally.
- [ ] Require MFA for all users if your use case demands it: Admin Panel → Settings → **Require two-factor authentication (2FA)**. Note: you must secure your own admin account first, with either TOTP or a registered passkey — the server refuses the toggle otherwise. A passkey satisfies the policy for everyone else too, so nobody is forced onto TOTP specifically.
- [ ] Disable open registration if you control who can access the instance. See [Admin-Users-and-Invites](Admin-Users-and-Invites).
- [ ] Rotate the JWT signing secret if a session may have been leaked: Admin Panel → Admin → Rotate JWT Secret (`POST /api/admin/rotate-jwt-secret`). This invalidates all active sessions immediately.
- [ ] Rotate the JWT signing secret if a session may have been leaked: Admin Panel → Settings → Danger Zone → **Rotate** (`POST /api/admin/rotate-jwt-secret`). This invalidates all active sessions immediately, including your own.
## Session Security
TREK stores sessions as JWTs in an httpOnly `trek_session` cookie (SameSite=Lax, 24-hour expiry). The `secure` flag is set automatically when `NODE_ENV=production` or `FORCE_HTTPS=true`. Tokens are also accepted via `Authorization: Bearer` header for MCP and API clients.
TREK stores sessions as JWTs in an httpOnly `trek_session` cookie (SameSite=Lax). A normal login expires after `SESSION_DURATION` (default 24 hours) and rides in a browser-session cookie that the browser drops when it closes; ticking **Remember me** issues a persistent cookie whose lifetime and JWT expiry are `SESSION_DURATION_REMEMBER` (default 30 days) — shorten it if a 30-day window is too wide for your threat model. The `secure` flag is set automatically when `NODE_ENV=production`, when `FORCE_HTTPS=true`, or when Express sees that the request arrived over TLS (`X-Forwarded-Proto: https`) — the last of those needs `trust proxy` active, which is automatic in production and otherwise means setting `TRUST_PROXY` yourself. Tokens are also accepted via `Authorization: Bearer` header for MCP and API clients.
- [ ] Ensure `FORCE_HTTPS=true` (or `NODE_ENV=production`) so the `trek_session` cookie carries the `secure` flag and is never sent over plain HTTP.
- [ ] Set `COOKIE_SECURE=false` only as a temporary escape hatch for LAN testing without TLS — do not use in production.
@@ -58,9 +59,9 @@ These limits are per source IP. If TREK is behind a reverse proxy, set `TRUST_PR
Helmet applies a strict CSP on all responses. Key directives:
- `default-src 'self'`
- `script-src 'self' 'wasm-unsafe-eval'` (no `unsafe-inline`)
- `object-src 'none'`
- `frame-src 'none'`
- `script-src 'self' 'wasm-unsafe-eval' 'unsafe-eval'` (no `unsafe-inline`; `'unsafe-eval'` is required by the in-browser HEIC converter, which initialises through `new Function()`)
- `object-src 'self'` (so same-origin file previews can embed PDFs via `<object>`/`<embed>`)
- `frame-src 'self'` (for the sandboxed plugin frames at `/plugin-frame/*`, which run at an opaque origin under their own CSP)
- `frameAncestors 'self'` (prevents clickjacking from external frames)
- `upgrade-insecure-requests` (added automatically when `FORCE_HTTPS=true`)
@@ -70,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).
- [ ] 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.
- [ ] 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`).
+3 -3
View File
@@ -14,7 +14,7 @@ Open the **Lists** tab inside the trip planner and select **Todo**. The Todo fea
The panel is divided into two columns by default:
- **Left sidebar** — category navigation, smart filters, sort toggle, and a completion progress card.
- **Left sidebar** — list navigation, smart filters, sort toggle, and a completion progress card.
- **Task list** — the tasks that match the active filter.
When you click a task, a **detail pane** opens as a third column on the right side of the task list (on desktop) or slides up as a bottom sheet (on mobile). The new-task form also opens as a modal overlay.
@@ -29,7 +29,7 @@ Each task has the following fields:
|---|---|
| Name | Required. |
| Checked | Boolean — marks the task done. |
| Category | Optional grouping label. |
| List | Optional grouping label. The sidebar heading is **Lists**, the button reads **Add list**, and a task without one shows **No list**. |
| Due date | Optional date. |
| Description | Optional free-text body. |
| Assignee | Optional trip member. |
@@ -55,7 +55,7 @@ Tasks with no priority set show no badge.
| **My tasks** | Unchecked tasks assigned to you. |
| **Overdue** | Unchecked tasks with a past due date. |
| **Done** | Checked tasks. |
| Per-category rows | All tasks in that specific category (checked and unchecked). |
| Per-list rows | All tasks in that specific list (checked and unchecked). |
## Sort by priority
+5 -3
View File
@@ -12,6 +12,8 @@ Open the **Transports** tab in the trip planner and click **Add**, or open the p
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.)
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.
The mode switch only appears when the trip has a **start date and an end date** — a transit search needs real dates to depart against. On a trip without dates the dialog opens directly on the manual form; add dates in the trip settings to get the Automated mode back.
- 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.
@@ -54,7 +56,7 @@ Once you select an airport, the **timezone** for that airport appears next to th
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.
If you use **car rentals** instead of your own Car, the departure field is labelled **Pickup date/time** and the arrival field is labelled **Return date/time**.
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
@@ -77,11 +79,11 @@ Trains created before this feature keep working: their existing train number/pla
Transport records with both endpoints set appear as lines on the trip map:
- **Flights** and **cruises** render as geodesic great-circle curves that follow the curvature of the Earth.
- **Flights**, **cruises** and **ferries** render as geodesic great-circle curves that follow the curvature of the Earth.
- **Cars**, **buses**, **taxis** and **bicycles** follow **real roads**, routed on demand via a public OSRM router (driving for car/bus/taxi, cycling for bicycle). A straight line shows while the route loads, and is kept if routing fails or the trip is over ~2000 km.
- **Trains** render as a straight polyline; a **multi-leg train** draws its full station chain (from → stop → to).
Confirmed bookings are drawn as solid lines; pending bookings use a dashed line. Endpoint markers are shown at each location. For flights, a midpoint label appears along the arc showing the route codes (e.g. ZRH → JFK) and flight duration and distance when enough screen space is available.
Confirmed bookings are drawn as solid lines; pending bookings use a dashed line. Each endpoint gets a pill-shaped marker with the transport icon. Turn on **Booking route labels** (Settings → General → Travel & map) to print the airport code or station name in the pill as well; the label appears once the two endpoints are far enough apart on screen.
See [Map-Features](Map-Features) for details on how these overlays work.
+13 -7
View File
@@ -15,7 +15,7 @@ The left column lists everyone who has access to the trip.
- The **trip owner** is marked with a crown badge.
- Your own entry is labeled **(you)**.
- Each non-owner member shows a remove button.
- A remove button appears on your own row, and on every non-owner row if you have the `member_manage` permission.
### Inviting Members
@@ -29,6 +29,12 @@ If you click the remove icon next to **your own** name, the action is labeled **
The trip owner cannot be removed through this panel.
### Transferring Ownership
Only the current owner sees a crown button next to each non-owner member. Click it and confirm the prompt — that member becomes the trip owner, you drop to a regular member, and the page reloads so the new permissions take effect everywhere.
Guests are never listed among the members, and the server refuses to hand ownership to one.
## Guest Members
Not everyone on a trip has — or wants — a TREK account. **Guests** let you add travel companions by name only, so you can assign them to costs, packing and tasks just like a real member, without creating a login for them.
@@ -43,7 +49,7 @@ Only the **trip owner** can manage guests (this is stricter than inviting member
Other members see the Guests section too (when guests exist) but cannot add, rename or remove them.
> If two guests share a name, TREK keeps them distinct internally (the second "Anna" becomes "Anna 2"), so assignments never get confused.
> Two guests may share the same name — TREK stores the name you type as-is and keeps the two apart by their internal id, so assignments never get confused. A guest name may be at most 50 characters.
### What a guest can be assigned to
@@ -87,7 +93,7 @@ Viewers do not need to log in. The link can be shared with anyone.
### Creating a Link
Click **Create share link**. A URL is generated and shown with a copy button.
Click **Create link** in the **Public Link** section. A URL is generated and shown with a copy button.
### Permission Toggles
@@ -95,11 +101,11 @@ Before or after creating the link, you can control what the link exposes. Toggle
| Toggle | Default | What it shows |
|---|---|---|
| **Map** | Always on | Trip map with place markers — cannot be disabled |
| **Map & Plan** | Always on | Trip map, the day plan (days, activities, day notes) and the place list — cannot be disabled |
| **Bookings** | On | Reservations and accommodations |
| **Packing** | Off | Packing list |
| **Budget** | Off | Budget tab |
| **Collab** | Off | Read-only view of notes, chat, and polls |
| **Costs** | Off | Costs tab |
| **Chat** | Off | Read-only view of the trip chat (collab notes and polls are never shared) |
Changes to toggles take effect immediately for an existing link.
@@ -118,7 +124,7 @@ Below the public share link, users with the `share_manage` permission can also c
Unlike the read-only share link, this one is **not anonymous**:
- Whoever opens it must have a TREK account and be **signed in**. If they are not, they are sent to the login page and returned to the invite afterwards — there is **no registration** from this link.
- Anyone who opens it while signed in is added to the trip as a **member** (the same access a manually-added member gets). The owner and existing members are simply taken straight to the trip.
- Opening it while signed in shows a confirmation card naming the trip. Clicking **Join trip** adds you as a **member** (the same access a manually-added member gets) and opens the trip. The owner and existing members see the same card and are simply taken to the trip when they accept.
- To invite people who do **not** yet have an account, use an admin invite link with an optional trip binding instead — see [Invite-Links](Invite-Links).
### Creating and revoking
+4 -9
View File
@@ -1,6 +1,6 @@
# Trip Planner Overview
The trip planner is the main workspace for building your itinerary. It opens automatically after you create a trip, or any time you click a trip card on the dashboard.
The trip planner is the main workspace for building your itinerary. You open it by clicking a trip card on the dashboard.
![Trip Planner](assets/TripPlannerWithPlane.png)
@@ -25,7 +25,7 @@ Each sidebar has a drag handle on its inner edge for resizing.
![Planner in its three-pane layout: the day plan sidebar with days, places, notes and flight entries on the left, the map in the centre, and the places sidebar with search and category filter on the right](assets/TripPlanner.png)
A **Day Detail overlay** slides in over the center pane when you open a specific day, showing weather, the day's notes, reservation summaries, and the accommodation block. It can be minimized without closing it.
A **Day Detail panel** floats over the map area when you open a specific day, showing the weather forecast, that day's reservations, and the accommodation block. It can be collapsed to a slim header bar without closing it.
## Tabs
@@ -35,7 +35,7 @@ The tab bar sits directly below the main navigation bar.
|---|---|
| **Plan** | The three-pane map view described above. Always visible. |
| **Transports** | Flights, trains, cars, cruises, and buses. |
| **Reservations** | Hotels, restaurants, events, tours, and other bookings. |
| **Bookings** | Hotels, restaurants, events, tours, and other bookings. |
| **Lists** | Packing list and to-do list. |
| **Costs** | Expense tracking, splitting, and settlement. |
| **Files** | Document manager for receipts, tickets, and other files. |
@@ -47,12 +47,7 @@ The active tab is saved in `sessionStorage` per trip, so switching between trips
## Mobile Layout
On screens narrower than 768 px, the two sidebars are not shown side-by-side. Instead, two floating buttons appear over the map:
- **Plan** — opens the Day Plan Sidebar as a full-screen overlay.
- **Places** — opens the Places Sidebar as a full-screen overlay.
Tap outside the overlay or use the close button to return to the map.
On screens narrower than 768 px, TREK does not squeeze the three-pane layout — it opens a dedicated mobile trip screen instead: a day-chip rail under the top bar, a switch between the day plan and a full-screen map, and a bottom dock for the other tabs. Tablets and desktops (768 px and up) get the three-pane layout described above.
## Undo
+26 -11
View File
@@ -105,7 +105,7 @@ If you are accessing TREK directly on `http://<host>:3000` with no proxy, remove
## Locked out of MFA / lost authenticator
**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 > Security** to disable or reconfigure MFA.
**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).
@@ -123,7 +123,7 @@ If you no longer have access to backup codes and cannot log in, an admin must di
**Cause:** Your reverse proxy has a default body size limit (commonly 1 MB or 10 MB) that is smaller than the backup ZIP. Backup archives include the full uploads directory and can be large.
**Fix:** Raise the body size limit in your proxy config. TREK's own backup upload cap is 500 MB. For nginx:
**Fix:** Raise the body size limit in your proxy config. TREK's own cap on the uploaded (compressed) archive is 500 MB by default. For nginx:
```nginx
client_max_body_size 500m;
@@ -131,6 +131,16 @@ client_max_body_size 500m;
Add this to the `location /` block (or the specific backup route). See [Reverse Proxy](Reverse-Proxy) and [Backups](Backups).
If the archive is genuinely larger than that, raise TREK's own caps too. There are two, and they are independent: one on the compressed upload, one on the total **decompressed** size of the archive (the zip-bomb guard). An archive that gets past the upload limit can still be refused part-way through extraction with `Backup exceeds the maximum decompressed size.`
```yaml
environment:
- BACKUP_UPLOAD_LIMIT_MB=2000 # compressed upload cap (default: 500)
- BACKUP_MAX_DECOMPRESSED_MB=20480 # decompressed cap (default: 5120, i.e. 5 GB)
```
Keep the proxy's `client_max_body_size` at or above `BACKUP_UPLOAD_LIMIT_MB`. Non-positive or invalid values for either variable abort startup.
---
## "Cannot find module" on startup
@@ -192,7 +202,7 @@ See [Encryption Key Rotation](Encryption-Key-Rotation) for how to retrieve or ro
## OIDC login returns "APP_URL is not configured"
**Cause:** When OIDC is enabled, TREK needs to know its own public URL to build the redirect URI. It resolves this from (1) `APP_URL` env var, (2) the first entry in `ALLOWED_ORIGINS`, (3) `http://localhost:<PORT>` as a last resort. If none of these are set and the request is not coming from localhost, TREK returns a 500 error.
**Cause:** When OIDC is enabled, TREK needs to know its own public URL to build the redirect URI. It resolves this from (1) `APP_URL` env var, (2) the first entry in `ALLOWED_ORIGINS`, (3) `http://localhost:<PORT>` as a last resort. Step (3) always produces a value, so the message in the heading is a guard that never actually fires. What you get instead, with `APP_URL` and `ALLOWED_ORIGINS` both unset, is a redirect URI of `http://localhost:<PORT>/api/auth/oidc/callback` — and the provider rejects it as an unregistered `redirect_uri`.
**Fix:** Set `APP_URL` to the public URL of your instance:
@@ -205,7 +215,7 @@ environment:
## OIDC login fails with issuer mismatch
**Cause:** TREK validates that the `issuer` field in the provider's discovery document exactly matches the configured `OIDC_ISSUER`. A trailing-slash difference (e.g. `https://auth.example.com` vs `https://auth.example.com/`) is enough to fail.
**Cause:** TREK validates that the `issuer` field in the provider's discovery document matches the configured `OIDC_ISSUER`. Trailing slashes are stripped from both sides before the comparison, so `https://auth.example.com` and `https://auth.example.com/` are the same value here — the difference is somewhere else: a realm path the provider adds, an internal hostname configured where the provider advertises the public one, or `http://` against `https://`.
**Fix:** Check the exact issuer value your provider advertises and match it:
@@ -215,19 +225,24 @@ curl -s https://<your-oidc-issuer>/.well-known/openid-configuration | jq .issuer
Set `OIDC_ISSUER` to that exact string.
> **Note:** The mismatch is only fatal while no custom discovery URL is set. With `OIDC_DISCOVERY_URL` configured — which is how Authentik realm paths are usually wired up — TREK treats the discovery document's issuer as the canonical one, logs `[OIDC] Discovery doc issuer … differs from configured OIDC_ISSUER …` and continues.
---
## OIDC login fails when provider is on a private/internal network
**Cause:** TREK's SSRF guard blocks outbound requests to private IP ranges by default. If your OIDC provider (e.g. Keycloak, Authentik) is running on an internal address, the discovery document fetch will be blocked with: `Requests to private/internal network addresses are not allowed.`
**Cause:** Not the SSRF guard, despite what it looks like. All four OIDC calls — discovery, token, userinfo, JWKS — go through the admin-configured fetch path, which deliberately **allows** loopback and private/LAN targets: a Keycloak or Authentik on `192.168.x` or `10.x` is a supported setup and needs no extra variable. `ALLOW_INTERNAL_NETWORK` belongs to the guard on *user*-supplied URLs and changes nothing about OIDC. The only addresses that path refuses are link-local and cloud-metadata ones (`169.254.0.0/16`, `fe80::/10`), which fail with `Requests to link-local / cloud-metadata addresses are not allowed`.
**Fix:**
**Fix:** Look for the reasons an internal provider actually fails. A failed discovery fetch answers `500 { "error": "OIDC login failed" }` and logs the real message as `[OIDC] Login error: …`, so start there:
```yaml
environment:
- ALLOW_INTERNAL_NETWORK=true
```bash
docker logs <container> 2>&1 | grep "OIDC"
```
- **The container cannot reach the issuer.** Test from inside it, not from your desktop — the container has its own DNS and its own network: `docker exec <container> wget -qO- https://<issuer>/.well-known/openid-configuration`. A `Could not resolve hostname` in the log is this.
- **The issuer is not HTTPS.** In production TREK refuses a plain-HTTP issuer up front with `400 { "error": "OIDC issuer must use HTTPS in production" }`, before any request goes out.
- **The provider's certificate is not trusted by the container.** A self-signed certificate on an internal Keycloak fails the TLS handshake; issue it from a CA the container trusts.
---
## Password reset emails are not delivered / SMTP is silent
@@ -262,7 +277,7 @@ environment:
- ALLOWED_ORIGINS=https://trek.example.com,https://other.example.com
```
If `ALLOWED_ORIGINS` is not set, TREK allows all origins (development default). See [Environment Variables](Environment-Variables).
If `ALLOWED_ORIGINS` is not set, the default is **same-origin only** — cross-origin browser requests are rejected — because every shipped deployment path (Dockerfile, `docker-compose.yml`, the Helm chart) runs with `NODE_ENV=production`. Allowing any origin is the development default, and only applies outside production. See [Environment Variables](Environment-Variables).
---
@@ -278,7 +293,7 @@ If `ALLOWED_ORIGINS` is not set, TREK allows all origins (development default).
**Fix:**
- Code `4001`: Log out and log back in. If it persists, check that your reverse proxy is not stripping the `token` query parameter from the WebSocket upgrade request.
- Code `4403`: The user must enable MFA in **Settings > Security**, or an admin can disable the global MFA requirement in **Admin > Settings**.
- Code `4403`: The user must enable MFA in **Settings > Account**, or an admin can disable the global MFA requirement in **Admin > Settings**.
---
+11 -7
View File
@@ -8,11 +8,11 @@ TREK supports Time-based One-Time Password (TOTP) two-factor authentication, com
## Setting up 2FA
Go to **Settings → Account** and click **"Set up two-factor authentication"**.
Go to **Settings → Account**, find the **Two-factor authentication (2FA)** section and click **Set up authenticator**.
1. A QR code and a text secret are displayed. Scan the QR code with your authenticator app.
> **Note:** The setup session expires after **15 minutes**. If you do not complete setup within that window, start again.
2. Enter the 6-digit code shown in your authenticator app and click **Confirm**.
2. Enter the 6-digit code shown in your authenticator app and click **Enable 2FA**.
3. Save your **10 backup codes**. These are single-use codes shown only once — store them somewhere safe (a password manager, printed paper). Each code has the format `XXXX-XXXX`.
4. 2FA is now active on your account.
@@ -25,18 +25,22 @@ After entering your email and password, TREK shows a second prompt for your TOTP
## Disabling 2FA
Go to **Settings → Account** and click **"Disable two-factor authentication"**. You must provide both:
Go to **Settings → Account** and use the **Disable 2FA** form. You must provide both:
- Your current account **password**
- A valid **TOTP code** from your authenticator app
> **Note:** You cannot disable 2FA while the admin has required it for all users (see below).
> **Note:** You cannot disable 2FA while the admin has required it for all users (see below). Owning a passkey does not lift that restriction.
## Admin-enforced 2FA
An admin can require 2FA for all users. Before enabling this setting the admin must have 2FA active on their own account — the server rejects the change otherwise.
An admin can require 2FA for all users. Before enabling this setting the admin must have secured their own account with either an authenticator app or a passkey — the server rejects the change otherwise with *Secure your own account with two-factor authentication or a passkey before requiring it for all users.*
If the setting is active and your account does not have 2FA set up, any API request after login returns a 403 error and the client redirects you to **Settings → Account** with a prompt to complete 2FA setup. You cannot use the app until setup is complete. See [Admin-Permissions](Admin-Permissions).
If the setting is active and your account has neither 2FA nor a passkey, any API request after login returns a 403 error and the client redirects you to **Settings → Account** with a prompt to complete 2FA setup. You cannot use the app until setup is complete. See [Admin-Permissions](Admin-Permissions).
A user-verified **passkey** satisfies this policy the way a TOTP authenticator does: with at least one passkey on your account the API stops blocking you and you never need an authenticator app. That holds even if an admin later turns passkey login off again — an existing passkey keeps satisfying the policy. Real-time sync is the one exception: the WebSocket handshake accepts only TOTP, so a passkey-only account under this policy can use TREK normally but never receives live updates from other members. Nothing tells you — the client simply keeps retrying the connection.
Whether enrolling a passkey is a way *out* of the lockout depends on the instance. Passkey login is off by default (see [Passkeys](Passkeys)); where an admin has turned it on, the enrolment endpoints stay reachable while the rest of the API is blocked, so you can add a passkey from **Settings → Account** instead of setting up an authenticator app. Where it is off, TOTP setup is the only way to unblock yourself — or ask an admin to reset your 2FA.
> **Admin:** You can reset 2FA for a locked-out user from the admin panel. See [Admin-Users-and-Invites](Admin-Users-and-Invites).
@@ -57,4 +61,4 @@ The demo user account cannot enable or disable MFA.
---
**See also:** [Login-and-Registration](Login-and-Registration) · [Admin-Permissions](Admin-Permissions) · [Admin-Users-and-Invites](Admin-Users-and-Invites) · [User-Settings](User-Settings)
**See also:** [Login-and-Registration](Login-and-Registration) · [Passkeys](Passkeys) · [Admin-Permissions](Admin-Permissions) · [Admin-Users-and-Invites](Admin-Users-and-Invites) · [User-Settings](User-Settings)
+4 -4
View File
@@ -11,8 +11,8 @@ Back up your data first. Go to Admin Panel → Backups and create a manual backu
| Tag | Example | Behavior |
|---|---|---|
| `latest` | `mauriceboe/trek:latest` | Always the newest release across all major versions |
| Major version | `mauriceboe/trek:3` | Latest release pinned to that major version |
| Full version | `mauriceboe/trek:3.4.0` | Exact release; never changes |
| Major version | `mauriceboe/trek:4` | Latest release pinned to that major version |
| Full version | `mauriceboe/trek:4.0.0` | Exact release; never changes |
Use `latest` or a major-version tag if you want updates on each redeploy. Use a full version tag for explicit control — update by changing the tag, not by re-pulling.
@@ -28,7 +28,7 @@ This pulls the newest matching image and recreates the container with your exist
**Pinned full-version tag:**
Edit `docker-compose.yml`, update the tag in the `image:` line (e.g. `3.4.0` → `3.4.1`), then redeploy:
Edit `docker-compose.yml`, update the tag in the `image:` line (e.g. `4.0.0` → `4.0.1`), then redeploy:
```bash
docker compose up -d
@@ -113,7 +113,7 @@ Open the **Stacks** list, click the TREK stack, then click **Redeploy**.
![Re-pull image and redeploy switch ticked, with arrows pointing to the switch and the Update button](assets/portainer-force-pull.png)
**Pinned full-version tag** (e.g. `3.4.0`) — edit the stack, update the tag in the `image:` line, then click **Update the stack**. No re-pull switch needed; the tag change forces a fresh pull.
**Pinned full-version tag** (e.g. `4.0.0`) — edit the stack, update the tag in the `image:` line, then click **Update the stack**. No re-pull switch needed; the tag change forces a fresh pull.
![Edit stack page with an arrow pointing to the image tag in the compose editor](assets/portainer-update-version.png)
+10 -5
View File
@@ -12,14 +12,14 @@ If your account requires MFA setup, TREK redirects you directly to the **Account
| Tab | Purpose | Shown when |
|-----|---------|------------|
| General | Start page and start tab, currency, language, temperature unit, distance unit, time format, booking route labels, map POI pills, and blur booking codes | Always |
| General | Start page and start tab, currency, language, temperature unit, distance unit, time format, booking route labels, always show booking routes, map POI pills, blur booking codes, and optimize route from accommodation | Always |
| Appearance | Color mode, color scheme / accent, readability (transparency, reduce motion, density, text size), and which widgets appear on your dashboard | Always |
| Map | Map provider (Leaflet, Mapbox GL, or MapLibre GL), tile presets, map style and Mapbox token, 3D buildings, high-quality mode | Always |
| Notifications | Email, webhook, ntfy, and in-app notification preferences | Always |
| Integrations | Photo providers (Immich, Synology, etc.) and MCP OAuth clients / API tokens | Only when the Memories or MCP addon is enabled |
| Integrations | Photo providers (Immich, Synology, etc.), the AirTrail connection, your own AI parsing model, and MCP OAuth clients / API tokens | Only when a photo provider (Immich or Synology Photos), MCP, AirTrail or AI Parsing is enabled |
| Plugins | Per-user settings for installed plugins | Only when at least one plugin is installed |
| Offline | Cached trips, pending changes, re-sync and clear cache | Always |
| Account | Username, email, password, MFA (TOTP + backup codes), avatar, delete account | Always |
| Account | Username, email, password, MFA (TOTP + backup codes), passkeys, avatar, delete account | Always |
| About | App version, links to Ko-fi / Buy Me a Coffee / Discord / GitHub (bug reports, feature requests) / Wiki | Only when version metadata is available |
## General tab
@@ -42,8 +42,10 @@ The General tab controls the following preferences, all saved immediately on cha
**Travel & map**
- **Booking route labels** — show or hide station / airport names on booking route endpoint markers.
- **Always show booking routes** — draw the route for every flight, train and other booking on the map automatically, instead of switching it on per item.
- **Explore places on the map** — show a POI category pill on the trip map for finding nearby places from OpenStreetMap.
- **Blur booking codes** — blur confirmation codes and reference numbers (useful when screen-sharing).
- **Optimize route from accommodation** — when optimizing a day, start the route at the hotel you wake up in and end it at the one you check into that evening.
## Appearance tab
@@ -81,6 +83,7 @@ The Account tab lets you:
- Edit your **username** and **email address**.
- Change your **password** (hidden when the server runs in OIDC-only mode — see [OIDC-SSO](OIDC-SSO)).
- Set up or disable **two-factor authentication** (TOTP). After enabling MFA, backup codes are shown once and can be copied, downloaded, or printed. See [Two-Factor-Authentication](Two-Factor-Authentication).
- Manage your **passkeys** — add one (confirm your password, then follow your device prompt), rename it, or remove it. **Add a passkey** only appears when the instance has passkeys switched on and a WebAuthn domain resolves; an existing passkey stays listed either way so you can always clean it up. See [Passkeys](Passkeys).
- Upload or remove your **profile avatar**.
- **Delete your account** (irreversible; blocked if you are the only admin).
@@ -88,9 +91,11 @@ If your account was linked via SSO, an **SSO** badge appears next to your role a
## Integrations tab
The Integrations tab is only visible when the **Memories** or **MCP** addon is enabled. It contains:
The Integrations tab is only visible when a **photo provider** (Immich or Synology Photos), **MCP**, **AirTrail** or **AI Parsing** is enabled. Each section then appears with its own addon, so the tab holds only what your instance actually runs:
- **Photo Providers** — Configure Immich, Synology Photos, and other photo integrations (always shown when the Integrations tab is visible).
- **Photo Providers** (only when a photo provider is enabled) — Configure Immich, Synology Photos, and other photo integrations. One card appears per enabled provider.
- **AirTrail** (only when the AirTrail addon is enabled) — Instance URL and API key for your self-hosted AirTrail, plus a self-signed-certificate switch and a **Write changes back to AirTrail** toggle (off by default: AirTrail is the source of truth and TREK only reads from it). **Test connection** reports how many flights it can see. The key is stored encrypted and never prefilled — leaving the field blank keeps the stored one.
- **AI parsing** (only when the AI Parsing addon is enabled) — The provider (OpenAI or Anthropic), model and API key used to extract bookings from your uploaded files, plus **Send documents as images** for vision-capable models. This is the per-user fallback: it only takes effect when your administrator has not configured a model for the whole instance. A local Ollama endpoint is not offered here — that is set up once, instance-wide, in the admin settings (#1772). See [AI-Booking-Import](AI-Booking-Import).
- **MCP section** (only when MCP addon is enabled):
- Shows the MCP server endpoint URL.
- **OAuth Clients** sub-tab — Create and manage OAuth 2.1 clients (with redirect URIs and scopes). Quick-fill presets are provided for Claude.ai, Claude Desktop, Cursor, VS Code, Windsurf, and Zed. Active OAuth sessions can be viewed and revoked here.
+2
View File
@@ -62,8 +62,10 @@ Each toggle's icon in the toolbar is the marker it places, so you can see what a
- **Block weekends** — prevents logging on weekend days. You choose which days count as the weekend.
- **Week start** — Monday or Sunday.
- **Carry-over** — toggle as described above.
- **Vacation year** — Calendar, Fiscal or Hire date, as described under Leave year above. Unlike the rest of this panel it is personal to you rather than to the plan.
- **Company holidays** — enable a shared company holiday layer that any fused user can edit.
- **Public holidays** — add one or more country/region holiday calendars so that public holidays appear on the grid. Holiday data is fetched from the nager.at public holiday API. Each calendar has a label, a colour, and a country or region selector (sub-national regions such as German states or Swiss cantons are supported).
- **School holidays** — a second holiday layer with its own toggle, independent of the public one and purely visual. Each calendar has an optional label, a colour and a country; where the source splits a country up you also pick a region (a German state, a Swiss canton, a French académie) or a school holiday group (Belgium, the Netherlands), and the calendar cannot be added until you have. A few countries publish a single national calendar (Estonia, Ireland, Serbia) and need nothing beyond the country. School holiday data comes from the OpenHolidays API rather than nager.at and covers fewer countries, so only the supported ones appear in the picker. On the grid a school holiday day gets a coloured band along the bottom of the cell, split into up to three segments when several calendars cover the same day, and a day inside a break that carries nothing else (no logged entry, no company holiday, no public holiday) is washed in the calendar colour. School holidays never deduct from anyone's allowance.
## Inviting collaborators
+1 -1
View File
@@ -6,7 +6,7 @@ The What's Next widget shows the upcoming assigned places across all days of you
## Where to find it
Open the trip planner → **Collab** tab. On **desktop** the widget appears as a panel (alongside Chat, Notes, and Polls). On **mobile** it appears as a tab labeled "What's Next" with a sparkle icon in the Collab tab bar. The Collab addon must be enabled and the What's Next sub-feature must be turned on. See [Real-Time-Collaboration](Real-Time-Collaboration).
Open the trip planner → **Collab** tab. On **desktop** (window 1024px and wider) the widget appears as a panel alongside Chat, Notes, and Polls. In a narrower window the four panels collapse into a tab bar and it becomes a tab labeled "What's Next". On a **phone** (under 768px) the mobile trip planner takes over, and its Collab tab only offers Chat, Notes and Polls — the widget is not available there. The Collab addon must be enabled and the What's Next sub-feature must be turned on. See [Real-Time-Collaboration](Real-Time-Collaboration).
## What it shows