diff --git a/.dockerignore b/.dockerignore
index 65f3dcd0..f3e717c0 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -2,6 +2,7 @@ node_modules
client/node_modules
server/node_modules
client/dist
+shared/dist
data
uploads
.git
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index ef038083..21e518bf 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -102,16 +102,15 @@ jobs:
echo "VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "$STABLE → $NEW_VERSION ($BUMP)"
- # Update package.json files and Helm chart
- cd server && npm version "$NEW_VERSION" --no-git-tag-version && cd ..
- cd client && npm version "$NEW_VERSION" --no-git-tag-version && cd ..
+ # Update all workspace + root package.json files and the root lockfile in one shot
+ npm version "$NEW_VERSION" --workspaces --include-workspace-root --no-git-tag-version
sed -i "s/^version: .*/version: $NEW_VERSION/" charts/trek/Chart.yaml
sed -i "s/^appVersion: .*/appVersion: \"$NEW_VERSION\"/" charts/trek/Chart.yaml
# Commit and tag
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- git add server/package.json server/package-lock.json client/package.json client/package-lock.json charts/trek/Chart.yaml
+ git add package.json package-lock.json server/package.json client/package.json shared/package.json charts/trek/Chart.yaml
git commit -m "chore: bump version to $NEW_VERSION [skip ci]"
git tag "v$NEW_VERSION"
git push origin main --follow-tags
diff --git a/.github/workflows/lint-prettier.yml b/.github/workflows/lint-prettier.yml
new file mode 100644
index 00000000..41c74611
--- /dev/null
+++ b/.github/workflows/lint-prettier.yml
@@ -0,0 +1,53 @@
+name: Lint & Prettier
+on:
+ pull_request:
+ branches: [main, dev]
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: '24'
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Run lint & format check
+ id: checks
+ continue-on-error: true
+ run: |
+ cd shared
+ npm run lint
+ npm run format:check
+
+ - name: Comment on PR if checks failed
+ if: steps.checks.outcome == 'failure'
+ uses: actions/github-script@v7
+ with:
+ script: |
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: [
+ '## ❌ Lint & Prettier check failed',
+ '',
+ 'Please fix the issues locally by running the following commands inside the `shared` package:',
+ '',
+ '```bash',
+ 'cd shared',
+ 'npm run lint',
+ 'npm run format',
+ '```',
+ '',
+ 'Then commit and push the changes.',
+ ].join('\n'),
+ });
+
+ - name: Fail the job if checks failed
+ if: steps.checks.outcome == 'failure'
+ run: exit 1
\ No newline at end of file
diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml
index 9cc8577d..88755200 100644
--- a/.github/workflows/security.yml
+++ b/.github/workflows/security.yml
@@ -34,4 +34,5 @@ jobs:
command: cves
image: trek:scan
only-severities: critical,high
+ only-fixed: true
exit-code: true
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 6c11b481..20f1864b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -8,10 +8,47 @@ on:
branches: [main, dev]
paths:
- 'server/**'
- - '.github/workflows/test.yml'
- 'client/**'
+ - 'shared/**'
+ - '.github/workflows/test.yml'
jobs:
+ i18n-parity:
+ name: i18n Key Parity
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v6
+
+ - uses: actions/setup-node@v6
+ with:
+ node-version: 24
+
+ - name: Check i18n key parity
+ run: node shared/scripts/i18n-parity.mjs --strict
+
+ shared-contracts:
+ name: Shared Contracts (Zod)
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v6
+
+ - uses: actions/setup-node@v6
+ with:
+ node-version: 24
+ cache: npm
+ cache-dependency-path: package-lock.json
+
+ - name: Install dependencies
+ run: npm ci --workspace shared
+
+ - name: Typecheck
+ run: cd shared && npm run typecheck
+
+ - name: Run tests
+ run: cd shared && npm test
+
server-tests:
name: Server Tests
runs-on: ubuntu-latest
@@ -21,12 +58,33 @@ jobs:
- uses: actions/setup-node@v6
with:
- node-version: 22
+ node-version: 24
cache: npm
- cache-dependency-path: server/package-lock.json
+ cache-dependency-path: package-lock.json
- name: Install dependencies
- run: cd server && npm ci
+ run: npm ci
+
+ - name: Ensure @swc/core's Linux binary for unplugin-swc
+ # The lockfile was generated on Windows and omits @swc/core's Linux
+ # optional native binary, so npm ci/install skips it on the runner.
+ # Install the matching version explicitly so the server's SWC transform
+ # (server/vitest.config.ts) can load.
+ run: |
+ SWC_VERSION=$(node -p "require('@swc/core/package.json').version")
+ npm install --no-save --legacy-peer-deps "@swc/core-linux-x64-gnu@$SWC_VERSION"
+
+ - name: Build shared
+ run: npm run build --workspace=shared
+
+ - name: Build server (tsc -> dist)
+ run: cd server && npm run build
+
+ - name: Typecheck
+ run: cd server && npm run typecheck
+
+ - name: Lint
+ run: cd server && npm run lint:check
- name: Run tests
run: cd server && npm run test:coverage
@@ -48,12 +106,24 @@ jobs:
- uses: actions/setup-node@v6
with:
- node-version: 22
+ node-version: 24
cache: npm
- cache-dependency-path: client/package-lock.json
+ cache-dependency-path: package-lock.json
- name: Install dependencies
- run: cd client && npm ci
+ run: npm ci --workspace shared && npm ci --workspace client
+
+ - name: Build shared
+ run: npm run build --workspace=shared
+
+ - name: Typecheck
+ run: cd client && npm run typecheck
+
+ - name: Lint
+ run: cd client && npm run lint:check
+
+ - name: Page pattern check
+ run: cd client && npm run lint:pages
- name: Run tests
run: cd client && npm run test:coverage
diff --git a/.gitignore b/.gitignore
index c796e687..b1e4bdf2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,8 @@ node_modules/
# Build output
client/dist/
+server/dist/
+shared/dist/
server/public/*
!server/public/.gitkeep
diff --git a/Dockerfile b/Dockerfile
index c16cc7b8..d6463fd7 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,31 +1,97 @@
-# Stage 1: Build React client
+# ── Stage 0: gosu ────────────────────────────────────────────────────────────
+# Rebuild gosu with a current Go toolchain so the runtime image ships no stale
+# Go stdlib (Debian's apt gosu is built with an old Go that trips CVE scanners).
+# The binary and its runtime behaviour are identical to the apt package.
+FROM golang:1.25-alpine AS gosu-build
+RUN CGO_ENABLED=0 GOBIN=/out go install github.com/tianon/gosu@latest
+
+# ── Stage 1: shared ──────────────────────────────────────────────────────────
+FROM node:24-alpine AS shared-builder
+WORKDIR /app
+COPY package.json package-lock.json ./
+COPY shared/package.json ./shared/
+RUN npm ci --workspace=shared
+COPY shared/ ./shared/
+RUN npm run build --workspace=shared
+
+# ── Stage 2: client ──────────────────────────────────────────────────────────
FROM node:24-alpine AS client-builder
-WORKDIR /app/client
-COPY client/package*.json ./
-RUN npm ci
-COPY client/ ./
-RUN npm run build
+WORKDIR /app
+COPY package.json package-lock.json ./
+COPY shared/package.json ./shared/
+COPY client/package.json ./client/
+RUN npm ci --workspace=client
+COPY --from=shared-builder /app/shared/dist ./shared/dist
+COPY client/ ./client/
+RUN npm run build --workspace=client
-# Stage 2: Production server
-FROM node:24-alpine
+# ── Stage 3: server ──────────────────────────────────────────────────────────
+# --ignore-scripts skips native builds (better-sqlite3); they happen in the production stage.
+FROM node:24-alpine AS server-builder
+WORKDIR /app
+COPY package.json package-lock.json ./
+COPY shared/package.json ./shared/
+COPY server/package.json ./server/
+RUN npm ci --workspace=server --ignore-scripts
+COPY --from=shared-builder /app/shared/dist ./shared/dist
+COPY server/ ./server/
+RUN npm run build --workspace=server
+# ── Stage 4: production runtime ──────────────────────────────────────────────
+FROM node:24-trixie-slim
WORKDIR /app
-# Timezone support + native deps (better-sqlite3 needs build tools)
-COPY server/package*.json ./
-RUN apk add --no-cache tzdata dumb-init su-exec python3 make g++ && \
- npm ci --production && \
- rm package-lock.json && \
- apk del python3 make g++ && \
- rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx
+# Workspace manifests only — source never enters this stage.
+COPY package.json package-lock.json ./
+COPY shared/package.json ./shared/
+COPY server/package.json ./server/
-COPY server/ ./
-COPY --from=client-builder /app/client/dist ./public
-COPY --from=client-builder /app/client/public/fonts ./public/fonts
+# better-sqlite3 native addon requires build tools (purged after compile).
+# kitinerary-extractor for booking-confirmation import:
+# amd64 — static binary from KDE CDN (glibc 2.17+; wget stays for healthcheck)
+# arm64 — apt package (KDE publishes no arm64 static binary)
+RUN apt-get update && \
+ apt-get install -y --no-install-recommends tzdata dumb-init wget ca-certificates python3 build-essential && \
+ npm ci --workspace=server --omit=dev && \
+ ARCH=$(dpkg --print-architecture) && \
+ if [ "$ARCH" = "amd64" ]; then \
+ wget -qO /tmp/ki.tgz https://cdn.kde.org/ci-builds/pim/kitinerary/release-26.04/linux/kitinerary-extractor-x86_64-26.04.2.tgz && \
+ echo "ba5cfb4a2353157c8f54cbeaea0097c5bf2c3a810e0342f63d6e524826176628 /tmp/ki.tgz" | sha256sum -c && \
+ tar -xz -C /usr/local -f /tmp/ki.tgz bin/kitinerary-extractor share/locale && \
+ rm /tmp/ki.tgz; \
+ else \
+ apt-get install -y --no-install-recommends libkitinerary-bin && \
+ ln -sf "$(find /usr/lib -name kitinerary-extractor -type f | head -1)" /usr/local/bin/kitinerary-extractor; \
+ fi && \
+ apt-get purge -y python3 build-essential && \
+ apt-get autoremove -y && \
+ rm -rf /var/lib/apt/lists/* /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx
-RUN rm -f package-lock.json && \
- mkdir -p /app/data/logs /app/uploads/files /app/uploads/covers /app/uploads/avatars /app/uploads/photos && \
- mkdir -p /app/server && ln -s /app/uploads /app/server/uploads && ln -s /app/data /app/server/data && \
+# gosu rebuilt with a current Go toolchain (stage 0) — used by CMD to drop to node.
+COPY --from=gosu-build /out/gosu /usr/local/bin/gosu
+
+ENV XDG_CACHE_HOME=/tmp/kf6-cache
+# Prevent Qt from probing for a display in headless containers.
+ENV QT_QPA_PLATFORM=offscreen
+# Fixed path for both amd64 (static binary) and arm64 (symlink to apt binary).
+# Override with KITINERARY_EXTRACTOR_PATH if you install it elsewhere.
+ENV KITINERARY_EXTRACTOR_PATH=/usr/local/bin/kitinerary-extractor
+
+COPY --from=server-builder /app/server/dist ./server/dist
+# Runtime data assets read from server/assets at runtime: airports.json (flight
+# transport search) and atlas/*.geojson.gz (Atlas country/region map). The build
+# only emits dist, so these must be copied explicitly or the features silently
+# degrade to empty in the image.
+COPY --from=server-builder /app/server/assets ./server/assets
+# tsconfig-paths/register reads this at runtime to resolve MCP SDK paths.
+COPY server/tsconfig.json ./server/
+COPY --from=shared-builder /app/shared/dist ./shared/dist
+COPY --from=client-builder /app/client/dist ./server/public
+COPY --from=client-builder /app/client/public/fonts ./server/public/fonts
+
+RUN mkdir -p /app/data/logs /app/uploads/files /app/uploads/covers /app/uploads/avatars /app/uploads/photos && \
+ ln -s /app/uploads /app/server/uploads && \
+ ln -s /app/data /app/server/data && \
chown -R node:node /app
ENV NODE_ENV=production
@@ -39,4 +105,8 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD wget -qO- http://localhost:3000/api/health || exit 1
ENTRYPOINT ["dumb-init", "--"]
-CMD ["sh", "-c", "chown -R node:node /app/data /app/uploads 2>/dev/null || true; exec su-exec node node --import tsx src/index.ts"]
+# Preflight: if the app code is missing, a volume was almost certainly mounted
+# over /app (it hides the image's node_modules + dist). Fail with actionable
+# guidance instead of a cryptic "Cannot find module 'tsconfig-paths/register'".
+# cd into server/ so tsconfig-paths/register finds tsconfig.json and ../node_modules resolves correctly.
+CMD ["sh", "-c", "if [ ! -f /app/server/dist/index.js ] || [ ! -d /app/node_modules/tsconfig-paths ]; then echo 'FATAL: TREK application files are missing from the image.'; echo 'A volume is likely mounted over /app, which hides the app code.'; echo 'Mount ONLY your data and uploads dirs: -v ./data:/app/data -v ./uploads:/app/uploads'; echo 'Do NOT mount a volume at /app. See the Troubleshooting section of the README.'; exit 1; fi; chown -R node:node /app/data /app/uploads 2>/dev/null || true; cd /app/server && exec gosu node node --require tsconfig-paths/register dist/index.js"]
diff --git a/NOTICE.md b/NOTICE.md
new file mode 100644
index 00000000..630a78f4
--- /dev/null
+++ b/NOTICE.md
@@ -0,0 +1,33 @@
+# Third-party data & attributions
+
+TREK bundles and uses third-party data that requires attribution.
+
+## geoBoundaries — country & sub-national boundaries
+
+The Atlas map's administrative boundaries (admin-0 countries and admin-1
+provinces/counties), shipped at `server/assets/atlas/admin0.geojson.gz` and
+`server/assets/atlas/admin1.geojson.gz` and generated by
+`server/scripts/build-atlas-geo.mjs`, are derived from **geoBoundaries**.
+
+> Runfola, D. et al. (2020) geoBoundaries: A global database of political
+> administrative boundaries. PLoS ONE 15(4): e0231866.
+> https://doi.org/10.1371/journal.pone.0231866
+
+geoBoundaries is licensed under **CC BY 4.0**
+(https://creativecommons.org/licenses/by/4.0/). Source: https://www.geoboundaries.org/
+
+The bundled files are simplified (coordinate-quantized) and re-tagged with the
+property names TREK consumes. Country borders (`admin0`) derive from the geoBoundaries
+CGAZ composite; sub-national regions (`admin1`) derive from the per-country open
+(gbOpen) release.
+
+## OpenStreetMap — geocoding
+
+Atlas reverse-geocodes places via the **Nominatim** service. Geocoding data is
+© OpenStreetMap contributors, licensed under the Open Database License (ODbL).
+https://www.openstreetmap.org/copyright
+
+## OurAirports — airport reference data
+
+`server/assets/airports.json` is built from **OurAirports**
+(https://ourairports.com/data/), released into the public domain.
diff --git a/README.md b/README.md
index b1b68317..3f4d9549 100644
--- a/README.md
+++ b/README.md
@@ -51,10 +51,10 @@ A self-hosted, real-time collaborative travel planner — with maps, budgets, pa
-
+
-
+
@@ -79,6 +79,7 @@ A self-hosted, real-time collaborative travel planner — with maps, budgets, pa
- **Drag & drop planner** — organise places into day plans with reordering and cross-day moves
- **Interactive map** — Leaflet or Mapbox GL with 3D buildings, terrain, photo markers, clustering, route visualization
- **Place search** — Google Places (photos, ratings, hours) or OpenStreetMap (free, no API key)
+- **Place import** — shared Google Maps / Naver Maps lists, plus GPX and KML/KMZ/GeoJSON map files
- **Day notes** — timestamped, icon-tagged notes with drag-and-drop reordering
- **Route optimisation** — auto-sort places and export to Google Maps
- **Weather forecasts** — 16-day via Open-Meteo (no key) + historical climate fallback
@@ -89,8 +90,8 @@ A self-hosted, real-time collaborative travel planner — with maps, budgets, pa
#### 🧳 Travel management
-- **Reservations** — flights, accommodations, restaurants with status, confirmation numbers, files
-- **Budget tracking** — category-based expenses with pie chart, per-person / per-day splits, multi-currency
+- **Reservations** — flights, accommodations, restaurants with status, confirmation numbers, files; import from booking confirmation emails and PDFs ([KDE Itinerary](https://invent.kde.org/pim/kitinerary))
+- **Costs** — track and split trip expenses (Splitwise-style): per-person / per-day breakdowns, settle-up, multi-currency
- **Packing lists** — categories, templates, user assignment, progress tracking
- **Bag tracking** — optional weight tracking with iOS-style distribution
- **Document manager** — attach docs, tickets, PDFs to trips / places / reservations (≤ 50 MB each)
@@ -108,6 +109,7 @@ A self-hosted, real-time collaborative travel planner — with maps, budgets, pa
- **Invite links** — one-time or reusable links with expiry
- **SSO (OIDC)** — Google, Apple, Authentik, Keycloak, or any OIDC provider
- **2FA** — TOTP + backup codes
+- **Passkeys** — passwordless WebAuthn login (fingerprint / face / PIN / security key), admin-toggleable
- **Collab suite** — group chat, shared notes, polls, day check-ins
@@ -128,13 +130,13 @@ A self-hosted, real-time collaborative travel planner — with maps, budgets, pa
#### 🧩 Addons (admin-toggleable)
- **Lists** — packing lists + to-dos with templates, member assignments, optional bag tracking
-- **Budget** — expense tracker with splits, pie chart, multi-currency
+- **Costs** — expense tracker with splits and settle-up (who owes whom), multi-currency
- **Documents** — file attachments on trips, places, and reservations
- **Collab** — chat, notes, polls, day-by-day attendance
- **Vacay** — personal vacation planner with calendar, 100+ country holidays, carry-over tracking
- **Atlas** — world map of visited countries, bucket list, travel stats, streak tracking, liquid-glass UI
- **Journey** — magazine-style travel journal with entries, photos (Immich/Synology), maps, moods
-- **Naver List Import** — one-click import from shared Naver Maps lists
+- **AirTrail** — connect a self-hosted AirTrail instance to import and sync flights into reservations
- **MCP** — expose TREK to AI assistants via OAuth 2.1
@@ -156,8 +158,9 @@ A self-hosted, real-time collaborative travel planner — with maps, budgets, pa
#### ⚙️ Admin & customisation
- **Dashboard views** — card grid or compact list · **Dark mode** — full theme with matching status bar
-- **15 languages** — EN, DE, ES, FR, IT, NL, HU, RU, ZH, ZH-TW, PL, CS, AR (RTL), BR, ID
+- **20 languages** — EN, DE, ES, FR, IT, NL, HU, RU, ZH, ZH-TW, PL, CS, AR (RTL), BR, ID, TR, JA, KO, UK, GR
- **Admin panel** — users, invites, packing templates, categories, addons, API keys, backups, GitHub history
+- **Notifications** — per-user preferences across email (SMTP), webhook, ntfy, and an in-app notification center
- **Auto-backups** — scheduled with configurable retention · **Units** — °C/°F, 12h/24h, map tile sources, default coordinates
@@ -191,9 +194,9 @@ Open `http://localhost:3000`. On first boot TREK seeds an admin account — if y
+
{t('admin.addons.subtitleBefore')}{t('admin.addons.subtitleAfter')}