mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
Compare commits
15 Commits
v3.0.0-pre.59
...
v3.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
| ec1ed60117 | |||
| ed4c21eade | |||
| 9093948ff6 | |||
| 2cea4d73aa | |||
| a2a6f52e6e | |||
| 0978b40b6d | |||
| 6155b6dc86 | |||
| 314486325e | |||
| 523bca3a20 | |||
| d5be528d4b | |||
| 3ada075b1a | |||
| afce302b59 | |||
| 8e8433fa9d | |||
| ff42fa0b8c | |||
| ccea7f7a65 |
@@ -23,4 +23,4 @@ jobs:
|
|||||||
- name: Publish to GitHub wiki
|
- name: Publish to GitHub wiki
|
||||||
uses: Andrew-Chen-Wang/github-wiki-action@v5
|
uses: Andrew-Chen-Wang/github-wiki-action@v5
|
||||||
with:
|
with:
|
||||||
strategy: init
|
strategy: clone
|
||||||
|
|||||||
@@ -127,19 +127,23 @@ A self-hosted, real-time collaborative travel planner — with maps, budgets, pa
|
|||||||
|
|
||||||
#### 🧩 Addons (admin-toggleable)
|
#### 🧩 Addons (admin-toggleable)
|
||||||
|
|
||||||
|
- **Lists** — packing lists + to-dos with templates, member assignments, optional bag tracking
|
||||||
|
- **Budget** — expense tracker with splits, pie chart, 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
|
- **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
|
- **Atlas** — world map of visited countries, bucket list, travel stats, streak tracking, liquid-glass UI
|
||||||
- **Collab** — chat, notes, polls, day-by-day attendance
|
- **Journey** — magazine-style travel journal with entries, photos (Immich/Synology), maps, moods
|
||||||
- **Journey** — magazine-style travel journal with entries, photos, maps, moods
|
- **Naver List Import** — one-click import from shared Naver Maps lists
|
||||||
- **Dashboard widgets** — currency converter and timezone clocks
|
- **MCP** — expose TREK to AI assistants via OAuth 2.1
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
#### 🤖 AI / MCP
|
#### 🤖 AI / MCP
|
||||||
|
|
||||||
- **Built-in MCP server** — OAuth 2.1 authenticated. 80+ tools, 27 resources
|
- **Built-in MCP server** — OAuth 2.1 authenticated. 150+ tools, 30 resources
|
||||||
- **Granular scopes** — 24 OAuth scopes across 13 permission groups
|
- **Granular scopes** — 27 OAuth scopes across 13 permission groups
|
||||||
- **Full automation** — AI can create trips, plan days, build packing lists, manage budgets, mark countries visited
|
- **Full automation** — AI can create trips, plan days, build packing lists, manage budgets, mark countries visited
|
||||||
- **Pre-built prompts** — `trip-summary`, `packing-list`, `budget-overview`
|
- **Pre-built prompts** — `trip-summary`, `packing-list`, `budget-overview`
|
||||||
- **Addon-aware** — exposes Atlas, Collab, Vacay when those addons are on
|
- **Addon-aware** — exposes Atlas, Collab, Vacay when those addons are on
|
||||||
@@ -152,7 +156,7 @@ A self-hosted, real-time collaborative travel planner — with maps, budgets, pa
|
|||||||
#### ⚙️ Admin & customisation
|
#### ⚙️ Admin & customisation
|
||||||
|
|
||||||
- **Dashboard views** — card grid or compact list · **Dark mode** — full theme with matching status bar
|
- **Dashboard views** — card grid or compact list · **Dark mode** — full theme with matching status bar
|
||||||
- **14 languages** — EN, DE, ES, FR, IT, NL, HU, RU, ZH, ZH-TW, PL, CS, AR (RTL), BR, ID
|
- **15 languages** — EN, DE, ES, FR, IT, NL, HU, RU, ZH, ZH-TW, PL, CS, AR (RTL), BR, ID
|
||||||
- **Admin panel** — users, invites, packing templates, categories, addons, API keys, backups, GitHub history
|
- **Admin panel** — users, invites, packing templates, categories, addons, API keys, backups, GitHub history
|
||||||
- **Auto-backups** — scheduled with configurable retention · **Units** — °C/°F, 12h/24h, map tile sources, default coordinates
|
- **Auto-backups** — scheduled with configurable retention · **Units** — °C/°F, 12h/24h, map tile sources, default coordinates
|
||||||
|
|
||||||
@@ -172,7 +176,7 @@ ENCRYPTION_KEY=$(openssl rand -hex 32) docker run -d -p 3000:3000 \
|
|||||||
-v ./data:/app/data -v ./uploads:/app/uploads mauriceboe/trek
|
-v ./data:/app/data -v ./uploads:/app/uploads mauriceboe/trek
|
||||||
```
|
```
|
||||||
|
|
||||||
Open `http://localhost:3000`. The first user to register becomes admin.
|
Open `http://localhost:3000`. On first boot TREK seeds an admin account — if you set `ADMIN_EMAIL`/`ADMIN_PASSWORD` those are used, otherwise the credentials are printed to the container log (`docker logs trek`).
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
@@ -338,7 +342,8 @@ server {
|
|||||||
ssl_certificate /etc/ssl/fullchain.pem;
|
ssl_certificate /etc/ssl/fullchain.pem;
|
||||||
ssl_certificate_key /etc/ssl/privkey.pem;
|
ssl_certificate_key /etc/ssl/privkey.pem;
|
||||||
|
|
||||||
client_max_body_size 50m;
|
# 500 MB covers backup-restore uploads (capped at 500 MB server-side).
|
||||||
|
client_max_body_size 500m;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://localhost:3000;
|
proxy_pass http://localhost:3000;
|
||||||
@@ -355,6 +360,7 @@ server {
|
|||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
proxy_read_timeout 86400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
name: trek
|
name: trek
|
||||||
version: 2.9.14
|
version: 3.0.2
|
||||||
description: Minimal Helm chart for TREK app
|
description: Minimal Helm chart for TREK app
|
||||||
appVersion: "2.9.14"
|
appVersion: "3.0.2"
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "trek-client",
|
"name": "trek-client",
|
||||||
"version": "2.9.14",
|
"version": "3.0.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "trek-client",
|
"name": "trek-client",
|
||||||
"version": "2.9.14",
|
"version": "3.0.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-pdf/renderer": "^4.3.2",
|
"@react-pdf/renderer": "^4.3.2",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "trek-client",
|
"name": "trek-client",
|
||||||
"version": "2.9.14",
|
"version": "3.0.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useMemo } from 'react'
|
||||||
import { Plane, Train, Car, Ship } from 'lucide-react'
|
import { Plane, Train, Car, Ship } from 'lucide-react'
|
||||||
import Modal from '../shared/Modal'
|
import Modal from '../shared/Modal'
|
||||||
import CustomSelect from '../shared/CustomSelect'
|
import CustomSelect from '../shared/CustomSelect'
|
||||||
@@ -7,6 +7,8 @@ import AirportSelect, { type Airport } from './AirportSelect'
|
|||||||
import LocationSelect, { type LocationPoint } from './LocationSelect'
|
import LocationSelect, { type LocationPoint } from './LocationSelect'
|
||||||
import { useTranslation } from '../../i18n'
|
import { useTranslation } from '../../i18n'
|
||||||
import { useToast } from '../shared/Toast'
|
import { useToast } from '../shared/Toast'
|
||||||
|
import { useTripStore } from '../../store/tripStore'
|
||||||
|
import { useAddonStore } from '../../store/addonStore'
|
||||||
import { formatDate } from '../../utils/formatters'
|
import { formatDate } from '../../utils/formatters'
|
||||||
import type { Day, Reservation, ReservationEndpoint } from '../../types'
|
import type { Day, Reservation, ReservationEndpoint } from '../../types'
|
||||||
|
|
||||||
@@ -75,6 +77,8 @@ const defaultForm = {
|
|||||||
arrival_time: '',
|
arrival_time: '',
|
||||||
confirmation_number: '',
|
confirmation_number: '',
|
||||||
notes: '',
|
notes: '',
|
||||||
|
price: '',
|
||||||
|
budget_category: '',
|
||||||
meta_airline: '',
|
meta_airline: '',
|
||||||
meta_flight_number: '',
|
meta_flight_number: '',
|
||||||
meta_train_number: '',
|
meta_train_number: '',
|
||||||
@@ -94,6 +98,13 @@ interface TransportModalProps {
|
|||||||
export function TransportModal({ isOpen, onClose, onSave, reservation, days, selectedDayId }: TransportModalProps) {
|
export function TransportModal({ isOpen, onClose, onSave, reservation, days, selectedDayId }: TransportModalProps) {
|
||||||
const { t, locale } = useTranslation()
|
const { t, locale } = useTranslation()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
const isBudgetEnabled = useAddonStore(s => s.isEnabled('budget'))
|
||||||
|
const budgetItems = useTripStore(s => s.budgetItems)
|
||||||
|
const budgetCategories = useMemo(() => {
|
||||||
|
const cats = new Set<string>()
|
||||||
|
budgetItems.forEach(i => { if (i.category) cats.add(i.category) })
|
||||||
|
return Array.from(cats).sort()
|
||||||
|
}, [budgetItems])
|
||||||
const [form, setForm] = useState({ ...defaultForm })
|
const [form, setForm] = useState({ ...defaultForm })
|
||||||
const [isSaving, setIsSaving] = useState(false)
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
const [fromPick, setFromPick] = useState<EndpointPick>({})
|
const [fromPick, setFromPick] = useState<EndpointPick>({})
|
||||||
@@ -126,6 +137,8 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
|
|||||||
meta_train_number: meta.train_number || '',
|
meta_train_number: meta.train_number || '',
|
||||||
meta_platform: meta.platform || '',
|
meta_platform: meta.platform || '',
|
||||||
meta_seat: meta.seat || '',
|
meta_seat: meta.seat || '',
|
||||||
|
price: meta.price || '',
|
||||||
|
budget_category: (meta.budget_category && budgetItems.some(i => i.category === meta.budget_category)) ? meta.budget_category : '',
|
||||||
})
|
})
|
||||||
if (type === 'flight') {
|
if (type === 'flight') {
|
||||||
setFromPick({ airport: airportFromEndpoint(from) || undefined })
|
setFromPick({ airport: airportFromEndpoint(from) || undefined })
|
||||||
@@ -139,7 +152,7 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
|
|||||||
setFromPick({})
|
setFromPick({})
|
||||||
setToPick({})
|
setToPick({})
|
||||||
}
|
}
|
||||||
}, [isOpen, reservation, selectedDayId])
|
}, [isOpen, reservation, selectedDayId, budgetItems])
|
||||||
|
|
||||||
const set = (field: string, value: any) => setForm(prev => ({ ...prev, [field]: value }))
|
const set = (field: string, value: any) => setForm(prev => ({ ...prev, [field]: value }))
|
||||||
|
|
||||||
@@ -173,6 +186,10 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
|
|||||||
if (form.meta_platform) metadata.platform = form.meta_platform
|
if (form.meta_platform) metadata.platform = form.meta_platform
|
||||||
if (form.meta_seat) metadata.seat = form.meta_seat
|
if (form.meta_seat) metadata.seat = form.meta_seat
|
||||||
}
|
}
|
||||||
|
if (isBudgetEnabled) {
|
||||||
|
if (form.price) metadata.price = form.price
|
||||||
|
if (form.budget_category) metadata.budget_category = form.budget_category
|
||||||
|
}
|
||||||
|
|
||||||
const startDate = startDay?.date ?? null
|
const startDate = startDay?.date ?? null
|
||||||
const endDate = (endDay ?? startDay)?.date ?? null
|
const endDate = (endDay ?? startDay)?.date ?? null
|
||||||
@@ -200,6 +217,11 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
|
|||||||
endpoints,
|
endpoints,
|
||||||
needs_review: false,
|
needs_review: false,
|
||||||
}
|
}
|
||||||
|
if (isBudgetEnabled) {
|
||||||
|
(payload as any).create_budget_entry = form.price && parseFloat(form.price) > 0
|
||||||
|
? { total_price: parseFloat(form.price), category: form.budget_category || t(`reservations.type.${form.type}`) || 'Other' }
|
||||||
|
: { total_price: 0 }
|
||||||
|
}
|
||||||
await onSave(payload)
|
await onSave(payload)
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
toast.error(err instanceof Error ? err.message : t('common.unknownError'))
|
toast.error(err instanceof Error ? err.message : t('common.unknownError'))
|
||||||
@@ -422,6 +444,40 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
|
|||||||
style={{ ...inputStyle, resize: 'none', lineHeight: 1.5 }} />
|
style={{ ...inputStyle, resize: 'none', lineHeight: 1.5 }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Price + Budget Category */}
|
||||||
|
{isBudgetEnabled && (
|
||||||
|
<>
|
||||||
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<label style={labelStyle}>{t('reservations.price')}</label>
|
||||||
|
<input type="text" inputMode="decimal" value={form.price}
|
||||||
|
onChange={e => { const v = e.target.value; if (v === '' || /^\d*[.,]?\d{0,2}$/.test(v)) set('price', v.replace(',', '.')) }}
|
||||||
|
onPaste={e => { e.preventDefault(); let txt = e.clipboardData.getData('text').trim().replace(/[^\d.,-]/g, ''); const lc = txt.lastIndexOf(','), ld = txt.lastIndexOf('.'), dp = Math.max(lc, ld); if (dp > -1) { txt = txt.substring(0, dp).replace(/[.,]/g, '') + '.' + txt.substring(dp + 1) } else { txt = txt.replace(/[.,]/g, '') } set('price', txt) }}
|
||||||
|
placeholder="0.00"
|
||||||
|
style={inputStyle} />
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<label style={labelStyle}>{t('reservations.budgetCategory')}</label>
|
||||||
|
<CustomSelect
|
||||||
|
value={form.budget_category}
|
||||||
|
onChange={v => set('budget_category', v)}
|
||||||
|
options={[
|
||||||
|
{ value: '', label: t('reservations.budgetCategoryAuto') },
|
||||||
|
...budgetCategories.map(c => ({ value: c, label: c })),
|
||||||
|
]}
|
||||||
|
placeholder={t('reservations.budgetCategoryAuto')}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{form.price && parseFloat(form.price) > 0 && (
|
||||||
|
<div style={{ fontSize: 11, color: 'var(--text-faint)', marginTop: -4 }}>
|
||||||
|
{t('reservations.budgetHint')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2941,6 +2941,7 @@ function JourneyShareSection({ journeyId }: { journeyId: number }) {
|
|||||||
{[
|
{[
|
||||||
{ key: 'share_timeline' as const, label: t('journey.share.timeline'), icon: List },
|
{ key: 'share_timeline' as const, label: t('journey.share.timeline'), icon: List },
|
||||||
{ key: 'share_gallery' as const, label: t('journey.share.gallery'), icon: Grid },
|
{ key: 'share_gallery' as const, label: t('journey.share.gallery'), icon: Grid },
|
||||||
|
{ key: 'share_map' as const, label: t('journey.share.map'), icon: MapPin },
|
||||||
].map(({ key, label, icon: Icon }) => (
|
].map(({ key, label, icon: Icon }) => (
|
||||||
<button
|
<button
|
||||||
key={key}
|
key={key}
|
||||||
|
|||||||
@@ -448,7 +448,7 @@ export default function JourneyPublicPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-zinc-50 dark:bg-zinc-950">
|
<div className="min-h-screen bg-zinc-50 dark:bg-zinc-950">
|
||||||
{/* Hero */}
|
{/* Hero */}
|
||||||
<div className="relative text-center text-white" style={{ background: 'linear-gradient(135deg, #000 0%, #0f172a 50%, #1e293b 100%)', padding: '32px 20px 28px' }}>
|
<div className="relative text-center text-white" style={{ background: 'linear-gradient(135deg, #000 0%, #0f172a 50%, #1e293b 100%)', padding: '32px 20px 28px', overflow: 'hidden' }}>
|
||||||
{journey.cover_image && (
|
{journey.cover_image && (
|
||||||
<div style={{ position: 'absolute', inset: 0, backgroundImage: `url(/uploads/${journey.cover_image})`, backgroundSize: 'cover', backgroundPosition: 'center', opacity: 0.15 }} />
|
<div style={{ position: 'absolute', inset: 0, backgroundImage: `url(/uploads/${journey.cover_image})`, backgroundSize: 'cover', backgroundPosition: 'center', opacity: 0.15 }} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "trek-server",
|
"name": "trek-server",
|
||||||
"version": "2.9.14",
|
"version": "3.0.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "trek-server",
|
"name": "trek-server",
|
||||||
"version": "2.9.14",
|
"version": "3.0.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.28.0",
|
"@modelcontextprotocol/sdk": "^1.28.0",
|
||||||
"archiver": "^6.0.1",
|
"archiver": "^6.0.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "trek-server",
|
"name": "trek-server",
|
||||||
"version": "2.9.14",
|
"version": "3.0.2",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node --import tsx src/index.ts",
|
"start": "node --import tsx src/index.ts",
|
||||||
|
|||||||
@@ -2043,6 +2043,70 @@ function runMigrations(db: Database.Database): void {
|
|||||||
db.exec('CREATE INDEX IF NOT EXISTS idx_journey_entry_photos_entry ON journey_entry_photos(entry_id)');
|
db.exec('CREATE INDEX IF NOT EXISTS idx_journey_entry_photos_entry ON journey_entry_photos(entry_id)');
|
||||||
db.exec('CREATE INDEX IF NOT EXISTS idx_journey_entry_photos_photo ON journey_entry_photos(journey_photo_id)');
|
db.exec('CREATE INDEX IF NOT EXISTS idx_journey_entry_photos_photo ON journey_entry_photos(journey_photo_id)');
|
||||||
},
|
},
|
||||||
|
// Migration 122: Correct stale day_id / end_day_id on non-transport
|
||||||
|
// reservations. Migration 110 only backfilled transport types; tours,
|
||||||
|
// restaurants, events and "other" bookings kept a stale day_id from
|
||||||
|
// older code paths that often defaulted to the first day of the trip.
|
||||||
|
// Starting with v3.0.0 the planner renders reservations by day_id
|
||||||
|
// instead of reservation_time, so those stale rows show up on the
|
||||||
|
// wrong day. This migration nulls out day_id / end_day_id values that
|
||||||
|
// don't match the reservation's time and then backfills them from
|
||||||
|
// reservation_time / reservation_end_time.
|
||||||
|
() => {
|
||||||
|
db.exec(`
|
||||||
|
UPDATE reservations
|
||||||
|
SET day_id = NULL
|
||||||
|
WHERE reservation_time IS NOT NULL
|
||||||
|
AND day_id IS NOT NULL
|
||||||
|
AND type != 'hotel'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM days d
|
||||||
|
WHERE d.id = reservations.day_id
|
||||||
|
AND d.date = substr(reservations.reservation_time, 1, 10)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
db.exec(`
|
||||||
|
UPDATE reservations
|
||||||
|
SET end_day_id = NULL
|
||||||
|
WHERE reservation_end_time IS NOT NULL
|
||||||
|
AND end_day_id IS NOT NULL
|
||||||
|
AND type != 'hotel'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM days d
|
||||||
|
WHERE d.id = reservations.end_day_id
|
||||||
|
AND d.date = substr(reservations.reservation_end_time, 1, 10)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
db.exec(`
|
||||||
|
UPDATE reservations
|
||||||
|
SET day_id = (
|
||||||
|
SELECT d.id FROM days d
|
||||||
|
WHERE d.trip_id = reservations.trip_id
|
||||||
|
AND d.date = substr(reservations.reservation_time, 1, 10)
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
WHERE type != 'hotel'
|
||||||
|
AND reservation_time IS NOT NULL
|
||||||
|
AND day_id IS NULL
|
||||||
|
`);
|
||||||
|
|
||||||
|
db.exec(`
|
||||||
|
UPDATE reservations
|
||||||
|
SET end_day_id = (
|
||||||
|
SELECT d.id FROM days d
|
||||||
|
WHERE d.trip_id = reservations.trip_id
|
||||||
|
AND d.date = substr(reservations.reservation_end_time, 1, 10)
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
WHERE type != 'hotel'
|
||||||
|
AND reservation_end_time IS NOT NULL
|
||||||
|
AND end_day_id IS NULL
|
||||||
|
AND substr(reservations.reservation_end_time, 1, 10)
|
||||||
|
!= substr(reservations.reservation_time, 1, 10)
|
||||||
|
`);
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (currentVersion < migrations.length) {
|
if (currentVersion < migrations.length) {
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export async function discover(issuer: string, discoveryUrl?: string | null): Pr
|
|||||||
// Validate that the discovery doc's issuer matches the operator-configured
|
// Validate that the discovery doc's issuer matches the operator-configured
|
||||||
// one. A MITM or compromised doc could otherwise supply a crafted issuer
|
// one. A MITM or compromised doc could otherwise supply a crafted issuer
|
||||||
// that passes jwt.verify() because we used doc.issuer as the expected value.
|
// that passes jwt.verify() because we used doc.issuer as the expected value.
|
||||||
if (doc.issuer && doc.issuer !== issuer) {
|
if (doc.issuer && doc.issuer.replace(/\/+$/, '') !== issuer) {
|
||||||
throw new Error(`OIDC discovery issuer mismatch: expected "${issuer}", got "${doc.issuer}"`);
|
throw new Error(`OIDC discovery issuer mismatch: expected "${issuer}", got "${doc.issuer}"`);
|
||||||
}
|
}
|
||||||
doc._issuer = url;
|
doc._issuer = url;
|
||||||
|
|||||||
@@ -43,6 +43,24 @@ function loadEndpoints(reservationId: number): ReservationEndpoint[] {
|
|||||||
).all(reservationId) as ReservationEndpoint[];
|
).all(reservationId) as ReservationEndpoint[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve the day row whose date matches the date portion of an ISO-ish
|
||||||
|
// timestamp. Used to keep `day_id` / `end_day_id` in sync with
|
||||||
|
// `reservation_time` / `reservation_end_time` so non-transport bookings
|
||||||
|
// (tours, restaurants, events, ...) end up on the right day in the UI,
|
||||||
|
// which now filters by day_id instead of reservation_time.
|
||||||
|
function resolveDayIdFromTime(
|
||||||
|
tripId: string | number,
|
||||||
|
time: string | null | undefined,
|
||||||
|
): number | null {
|
||||||
|
if (!time) return null;
|
||||||
|
const datePart = time.slice(0, 10);
|
||||||
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(datePart)) return null;
|
||||||
|
const row = db
|
||||||
|
.prepare('SELECT id FROM days WHERE trip_id = ? AND date = ? LIMIT 1')
|
||||||
|
.get(tripId, datePart) as { id: number } | undefined;
|
||||||
|
return row?.id ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
const saveEndpoints = db.transaction((reservationId: number, endpoints: EndpointInput[]) => {
|
const saveEndpoints = db.transaction((reservationId: number, endpoints: EndpointInput[]) => {
|
||||||
db.prepare('DELETE FROM reservation_endpoints WHERE reservation_id = ?').run(reservationId);
|
db.prepare('DELETE FROM reservation_endpoints WHERE reservation_id = ?').run(reservationId);
|
||||||
const insert = db.prepare(`
|
const insert = db.prepare(`
|
||||||
@@ -160,13 +178,26 @@ export function createReservation(tripId: string | number, data: CreateReservati
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Derive day_id / end_day_id from reservation_time when the client
|
||||||
|
// didn't explicitly set them (non-hotel bookings only — hotels store
|
||||||
|
// their date range on the linked day_accommodation).
|
||||||
|
const resolvedType = type || 'other';
|
||||||
|
let resolvedDayId: number | null = day_id ?? null;
|
||||||
|
if (resolvedDayId == null && resolvedType !== 'hotel' && reservation_time) {
|
||||||
|
resolvedDayId = resolveDayIdFromTime(tripId, reservation_time);
|
||||||
|
}
|
||||||
|
let resolvedEndDayId: number | null = end_day_id ?? null;
|
||||||
|
if (resolvedEndDayId == null && resolvedType !== 'hotel' && reservation_end_time) {
|
||||||
|
resolvedEndDayId = resolveDayIdFromTime(tripId, reservation_end_time);
|
||||||
|
}
|
||||||
|
|
||||||
const result = db.prepare(`
|
const result = db.prepare(`
|
||||||
INSERT INTO reservations (trip_id, day_id, end_day_id, place_id, assignment_id, title, reservation_time, reservation_end_time, location, confirmation_number, notes, status, type, accommodation_id, metadata, needs_review)
|
INSERT INTO reservations (trip_id, day_id, end_day_id, place_id, assignment_id, title, reservation_time, reservation_end_time, location, confirmation_number, notes, status, type, accommodation_id, metadata, needs_review)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`).run(
|
`).run(
|
||||||
tripId,
|
tripId,
|
||||||
day_id || null,
|
resolvedDayId,
|
||||||
end_day_id ?? null,
|
resolvedEndDayId,
|
||||||
place_id || null,
|
place_id || null,
|
||||||
assignment_id || null,
|
assignment_id || null,
|
||||||
title,
|
title,
|
||||||
@@ -176,7 +207,7 @@ export function createReservation(tripId: string | number, data: CreateReservati
|
|||||||
confirmation_number || null,
|
confirmation_number || null,
|
||||||
notes || null,
|
notes || null,
|
||||||
status || 'pending',
|
status || 'pending',
|
||||||
type || 'other',
|
resolvedType,
|
||||||
resolvedAccommodationId,
|
resolvedAccommodationId,
|
||||||
metadata ? JSON.stringify(metadata) : null,
|
metadata ? JSON.stringify(metadata) : null,
|
||||||
needs_review ? 1 : 0
|
needs_review ? 1 : 0
|
||||||
@@ -290,6 +321,35 @@ export function updateReservation(id: string | number, tripId: string | number,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolvedType = (type ?? current.type) || 'other';
|
||||||
|
const nextReservationTime = resolvedType === 'hotel'
|
||||||
|
? null
|
||||||
|
: (reservation_time !== undefined ? (reservation_time || null) : current.reservation_time);
|
||||||
|
const nextReservationEndTime = resolvedType === 'hotel'
|
||||||
|
? null
|
||||||
|
: (reservation_end_time !== undefined ? (reservation_end_time || null) : current.reservation_end_time);
|
||||||
|
|
||||||
|
// day_id / end_day_id: honour an explicit value from the client,
|
||||||
|
// otherwise derive from the (possibly updated) reservation_time so the
|
||||||
|
// planner renders the booking on the correct day.
|
||||||
|
let nextDayId: number | null;
|
||||||
|
if (day_id !== undefined) {
|
||||||
|
nextDayId = day_id || null;
|
||||||
|
} else if (reservation_time !== undefined && resolvedType !== 'hotel') {
|
||||||
|
nextDayId = resolveDayIdFromTime(tripId, nextReservationTime);
|
||||||
|
} else {
|
||||||
|
nextDayId = current.day_id ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextEndDayId: number | null;
|
||||||
|
if (end_day_id !== undefined) {
|
||||||
|
nextEndDayId = end_day_id ?? null;
|
||||||
|
} else if (reservation_end_time !== undefined && resolvedType !== 'hotel') {
|
||||||
|
nextEndDayId = resolveDayIdFromTime(tripId, nextReservationEndTime);
|
||||||
|
} else {
|
||||||
|
nextEndDayId = (current as any).end_day_id ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
UPDATE reservations SET
|
UPDATE reservations SET
|
||||||
title = COALESCE(?, title),
|
title = COALESCE(?, title),
|
||||||
@@ -310,13 +370,13 @@ export function updateReservation(id: string | number, tripId: string | number,
|
|||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`).run(
|
`).run(
|
||||||
title || null,
|
title || null,
|
||||||
(type ?? current.type) === 'hotel' ? null : (reservation_time !== undefined ? (reservation_time || null) : current.reservation_time),
|
nextReservationTime,
|
||||||
(type ?? current.type) === 'hotel' ? null : (reservation_end_time !== undefined ? (reservation_end_time || null) : current.reservation_end_time),
|
nextReservationEndTime,
|
||||||
location !== undefined ? (location || null) : current.location,
|
location !== undefined ? (location || null) : current.location,
|
||||||
confirmation_number !== undefined ? (confirmation_number || null) : current.confirmation_number,
|
confirmation_number !== undefined ? (confirmation_number || null) : current.confirmation_number,
|
||||||
notes !== undefined ? (notes || null) : current.notes,
|
notes !== undefined ? (notes || null) : current.notes,
|
||||||
day_id !== undefined ? (day_id || null) : current.day_id,
|
nextDayId,
|
||||||
end_day_id !== undefined ? (end_day_id ?? null) : (current as any).end_day_id ?? null,
|
nextEndDayId,
|
||||||
place_id !== undefined ? (place_id || null) : current.place_id,
|
place_id !== undefined ? (place_id || null) : current.place_id,
|
||||||
assignment_id !== undefined ? (assignment_id || null) : current.assignment_id,
|
assignment_id !== undefined ? (assignment_id || null) : current.assignment_id,
|
||||||
status || null,
|
status || null,
|
||||||
|
|||||||
@@ -84,8 +84,9 @@ describe('GET /api/system-notices/active', () => {
|
|||||||
|
|
||||||
it('returns empty array for non-first-login user with no applicable notices', async () => {
|
it('returns empty array for non-first-login user with no applicable notices', async () => {
|
||||||
const { user } = createUser(testDb);
|
const { user } = createUser(testDb);
|
||||||
// login_count > 1 means firstLogin condition does not match for any notice
|
// login_count > 1 means firstLogin condition does not match for any notice;
|
||||||
testDb.prepare('UPDATE users SET login_count = 5 WHERE id = ?').run(user.id);
|
// first_seen_version >= 3.0.0 means existingUserBeforeVersion('3.0.0') also does not match
|
||||||
|
testDb.prepare('UPDATE users SET login_count = 5, first_seen_version = ? WHERE id = ?').run('3.0.0', user.id);
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get('/api/system-notices/active')
|
.get('/api/system-notices/active')
|
||||||
.set('Cookie', authCookie(user.id));
|
.set('Cookie', authCookie(user.id));
|
||||||
@@ -122,7 +123,7 @@ describe('GET /api/system-notices/active', () => {
|
|||||||
SYSTEM_NOTICES.push(TEST_NOTICE);
|
SYSTEM_NOTICES.push(TEST_NOTICE);
|
||||||
try {
|
try {
|
||||||
const { user } = createUser(testDb);
|
const { user } = createUser(testDb);
|
||||||
testDb.prepare('UPDATE users SET login_count = 5 WHERE id = ?').run(user.id);
|
testDb.prepare('UPDATE users SET login_count = 5, first_seen_version = ? WHERE id = ?').run('3.0.0', user.id);
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get('/api/system-notices/active')
|
.get('/api/system-notices/active')
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ If a toggle fails (e.g., network error), it rolls back to its previous state.
|
|||||||
|
|
||||||
Some addons require credentials or environment variables before they are functional:
|
Some addons require credentials or environment variables before they are functional:
|
||||||
|
|
||||||
- **Journey** — requires photo provider credentials (Immich or Synology Photos) configured per-user in their personal Settings. See [Photo-Providers](Photo-Providers).
|
- **Journey** — works without any external integration. To embed photos from Immich or Synology Photos, enable the corresponding photo-provider toggle listed under Journey, then configure credentials per-user in **Settings → Integrations**. See [Photo-Providers](Photo-Providers).
|
||||||
- **MCP** — requires `APP_URL` to be set so OAuth redirect URIs resolve correctly.
|
- **MCP** — requires `APP_URL` to be set so OAuth redirect URIs resolve correctly.
|
||||||
|
|
||||||
## Related pages
|
## Related pages
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ Verified in `server/src/config.ts` (line 107):
|
|||||||
|
|
||||||
## HTTPS / Proxy
|
## HTTPS / Proxy
|
||||||
|
|
||||||
These three variables work together behind a TLS-terminating reverse proxy. See [Reverse-Proxy] for the full explanation.
|
These three variables work together behind a TLS-terminating reverse proxy. See [Reverse-Proxy](Reverse-Proxy) for the full explanation.
|
||||||
|
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -62,7 +62,7 @@ These three variables work together behind a TLS-terminating reverse proxy. See
|
|||||||
|
|
||||||
## OIDC / SSO
|
## OIDC / SSO
|
||||||
|
|
||||||
For setup instructions, see [OIDC-SSO].
|
For setup instructions, see [OIDC-SSO](OIDC-SSO).
|
||||||
|
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -110,7 +110,7 @@ Both variables must be set together. If either is omitted, the account is create
|
|||||||
|
|
||||||
## MCP
|
## MCP
|
||||||
|
|
||||||
For setup instructions, see [MCP-Overview].
|
For setup instructions, see [MCP-Overview](MCP-Overview).
|
||||||
|
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -129,7 +129,7 @@ For setup instructions, see [MCP-Overview].
|
|||||||
|
|
||||||
## Related Pages
|
## Related Pages
|
||||||
|
|
||||||
- [Reverse-Proxy] — HTTPS proxy setup and the `FORCE_HTTPS` / `TRUST_PROXY` / `COOKIE_SECURE` trio
|
- [Reverse-Proxy](Reverse-Proxy) — HTTPS proxy setup and the `FORCE_HTTPS` / `TRUST_PROXY` / `COOKIE_SECURE` trio
|
||||||
- [OIDC-SSO] — complete OIDC configuration guide
|
- [OIDC-SSO](OIDC-SSO) — complete OIDC configuration guide
|
||||||
- [MCP-Overview] — MCP server setup and rate limiting
|
- [MCP-Overview](MCP-Overview) — MCP server setup and rate limiting
|
||||||
- [Encryption-Key-Rotation] — rotating the `ENCRYPTION_KEY` without losing data
|
- [Encryption-Key-Rotation](Encryption-Key-Rotation) — rotating the `ENCRYPTION_KEY` without losing data
|
||||||
|
|||||||
+13
-7
@@ -30,17 +30,23 @@ TREK is a self-hosted, real-time collaborative travel planner licensed under AGP
|
|||||||
- **Public Share Links** — share a read-only view of any trip
|
- **Public Share Links** — share a read-only view of any trip
|
||||||
|
|
||||||
### Addons _(admin-toggleable)_
|
### 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
|
||||||
|
- **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
|
- **Vacay** — personal vacation day planner with calendar view, public holidays, and carry-over tracking
|
||||||
- **Atlas** — interactive world map, bucket list, travel stats, continent breakdown
|
- **Atlas** — interactive world map, bucket list, travel stats, continent breakdown
|
||||||
- **Journey** — travel journal linking entries to trips, with contributor roles
|
- **Journey** — magazine-style travel journal with entries, photos (via Immich/Synology Photos), maps, and moods
|
||||||
- **Memories** — photo-focused trip memories
|
- **Naver List Import** — import places from shared Naver Maps lists
|
||||||
- **Collab** — group chat, shared notes, polls, and activity sign-ups
|
- **MCP** — expose TREK to AI assistants via the Model Context Protocol (OAuth 2.1)
|
||||||
- **Dashboard Widgets** — currency converter and timezone clock, toggled per user
|
|
||||||
|
> Dashboard widgets (currency converter and timezone clock) are per-user preferences, not an admin-toggleable addon — see [Dashboard-Widgets](Dashboard-Widgets).
|
||||||
|
|
||||||
### AI / MCP Integration
|
### AI / MCP Integration
|
||||||
- **MCP Server** — built-in Model Context Protocol server with OAuth 2.1 authentication
|
- **MCP Server** — built-in Model Context Protocol server with OAuth 2.1 authentication
|
||||||
- **80+ Tools** — create trips, plan itineraries, manage budgets, send messages, and more
|
- **150+ Tools** — create trips, plan itineraries, manage budgets, send messages, and more
|
||||||
- **24 OAuth Scopes** — granular permissions across 13 permission groups
|
- **30 Resources** — read-only `trek://` URIs for trips, days, places, budget, packing, journeys, and more
|
||||||
|
- **27 OAuth Scopes** — granular permissions across 13 permission groups
|
||||||
- **Pre-built Prompts** — `trip-summary`, `packing-list`, and `budget-overview` context loaders
|
- **Pre-built Prompts** — `trip-summary`, `packing-list`, and `budget-overview` context loaders
|
||||||
|
|
||||||
### Admin
|
### Admin
|
||||||
@@ -48,7 +54,7 @@ TREK is a self-hosted, real-time collaborative travel planner licensed under AGP
|
|||||||
- Addon management, API key storage, scheduled auto-backups
|
- Addon management, API key storage, scheduled auto-backups
|
||||||
- System notices for onboarding and announcements
|
- System notices for onboarding and announcements
|
||||||
|
|
||||||
> **Admin:** Most configuration lives in the Admin Panel. The first user to register becomes the admin automatically.
|
> **Admin:** Most configuration lives in the Admin Panel. On first boot TREK seeds an admin account automatically — credentials come from `ADMIN_EMAIL` / `ADMIN_PASSWORD` if set, otherwise a random password is printed to the container log.
|
||||||
|
|
||||||
## Get Started
|
## Get Started
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ ALLOWED_ORIGINS=https://trek.example.com
|
|||||||
APP_URL=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].
|
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).
|
||||||
|
|
||||||
## Start TREK
|
## Start TREK
|
||||||
|
|
||||||
@@ -111,10 +111,10 @@ docker compose logs -f
|
|||||||
|
|
||||||
This compose file is designed for deployments where a reverse proxy (nginx, Caddy, Traefik) terminates TLS in front of TREK. To enable HTTPS redirects and secure cookies, uncomment `FORCE_HTTPS=true` and `TRUST_PROXY=1`.
|
This compose file is designed for deployments where a reverse proxy (nginx, Caddy, Traefik) terminates TLS in front of TREK. To enable HTTPS redirects and secure cookies, uncomment `FORCE_HTTPS=true` and `TRUST_PROXY=1`.
|
||||||
|
|
||||||
See [Reverse-Proxy] for complete proxy configuration examples.
|
See [Reverse-Proxy](Reverse-Proxy) for complete proxy configuration examples.
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
- [Environment-Variables] — full variable reference
|
- [Environment-Variables](Environment-Variables) — full variable reference
|
||||||
- [Reverse-Proxy] — HTTPS configuration
|
- [Reverse-Proxy](Reverse-Proxy) — HTTPS configuration
|
||||||
- [Updating] — how to pull a new image
|
- [Updating](Updating) — how to pull a new image
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ Pass additional `-e` flags for timezone and CORS/email link support:
|
|||||||
-e ALLOWED_ORIGINS=https://trek.example.com \
|
-e ALLOWED_ORIGINS=https://trek.example.com \
|
||||||
```
|
```
|
||||||
|
|
||||||
See [Environment-Variables] for the full list.
|
See [Environment-Variables](Environment-Variables) for the full list.
|
||||||
|
|
||||||
## Volume Reference
|
## Volume Reference
|
||||||
|
|
||||||
@@ -66,11 +66,11 @@ docker logs trek
|
|||||||
|
|
||||||
## Limitations of `docker run`
|
## Limitations of `docker run`
|
||||||
|
|
||||||
A bare `docker run` command has no built-in secret management and is harder to reproduce after a system reboot. For production, see [Install-Docker-Compose], which adds security hardening (`read_only`, `cap_drop`, `cap_add`, `no-new-privileges`, `tmpfs`) and makes it easy to manage environment variables through a `.env` file.
|
A bare `docker run` command has no built-in secret management and is harder to reproduce after a system reboot. For production, see [Install-Docker-Compose](Install-Docker-Compose), which adds security hardening (`read_only`, `cap_drop`, `cap_add`, `no-new-privileges`, `tmpfs`) and makes it easy to manage environment variables through a `.env` file.
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
- [Reverse-Proxy] — HTTPS is required for PWA install and the `trek_session` cookie `secure` flag
|
- [Reverse-Proxy](Reverse-Proxy) — HTTPS is required for PWA install and the `trek_session` cookie `secure` flag
|
||||||
- [Install-Docker-Compose] — recommended for production
|
- [Install-Docker-Compose](Install-Docker-Compose) — recommended for production
|
||||||
- [Environment-Variables] — full list of configurable variables
|
- [Environment-Variables](Environment-Variables) — full list of configurable variables
|
||||||
- [Updating] — how to pull a new image without losing data
|
- [Updating](Updating) — how to pull a new image without losing data
|
||||||
|
|||||||
@@ -191,5 +191,5 @@ See the [`charts/README.md`](https://github.com/mauriceboe/TREK/blob/main/charts
|
|||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
- [Environment-Variables] — full variable reference
|
- [Environment-Variables](Environment-Variables) — full variable reference
|
||||||
- [Reverse-Proxy] — proxy configuration for non-Kubernetes deployments
|
- [Reverse-Proxy](Reverse-Proxy) — proxy configuration for non-Kubernetes deployments
|
||||||
|
|||||||
@@ -69,5 +69,5 @@ On first boot, TREK automatically creates an admin account. The credentials are
|
|||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
- [Environment-Variables] — complete variable reference
|
- [Environment-Variables](Environment-Variables) — complete variable reference
|
||||||
- [Updating] — how to pull a new image on Unraid
|
- [Updating](Updating) — how to pull a new image on Unraid
|
||||||
|
|||||||
@@ -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.
|
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:** Two things must be enabled for photo providers to appear in Settings: the **Memories addon** and the **individual photo provider** (Immich or Synology Photos). Both are toggled separately in **Admin → Addons**. See [Admin-Addons](Admin-Addons). If your provider is on a local or private network, the server must be configured to allow internal network access. See [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, 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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -60,7 +60,7 @@ You will be prompted to change the password on first login.
|
|||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
- [Install-Docker-Compose] — production setup with security hardening
|
- [Install-Docker-Compose](Install-Docker-Compose) — production setup with security hardening
|
||||||
- [Reverse-Proxy] — put TREK behind HTTPS (required for PWA install and secure cookies)
|
- [Reverse-Proxy](Reverse-Proxy) — put TREK behind HTTPS (required for PWA install and secure cookies)
|
||||||
- [Environment-Variables] — full configuration reference
|
- [Environment-Variables](Environment-Variables) — full configuration reference
|
||||||
- [Admin-Panel-Overview] — explore what the admin panel can do
|
- [Admin-Panel-Overview](Admin-Panel-Overview) — explore what the admin panel can do
|
||||||
|
|||||||
@@ -98,9 +98,9 @@ Four variables control how TREK behaves behind a proxy. They work as a group:
|
|||||||
|
|
||||||
If you access TREK directly on `http://<host>:3000` without a proxy, leave `FORCE_HTTPS` unset and do not set `TRUST_PROXY`.
|
If you access TREK directly on `http://<host>:3000` without a proxy, leave `FORCE_HTTPS` unset and do not set `TRUST_PROXY`.
|
||||||
|
|
||||||
See [Environment-Variables] for full documentation of these and all other variables.
|
See [Environment-Variables](Environment-Variables) for full documentation of these and all other variables.
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
- [Environment-Variables] — full variable reference including OIDC
|
- [Environment-Variables](Environment-Variables) — full variable reference including OIDC
|
||||||
- [Install-Docker-Compose] — production compose file with proxy-ready env vars
|
- [Install-Docker-Compose](Install-Docker-Compose) — production compose file with proxy-ready env vars
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
# Tags and Categories
|
# Tags and Categories
|
||||||
|
|
||||||
TREK has a labeling system: **Global Place Categories** (admin-managed, shared across all users).
|
TREK has two independent labelling systems for places:
|
||||||
|
|
||||||
|
- **Global Place Categories** — admin-managed, shared across every user on the instance (e.g. `Restaurant`, `Museum`).
|
||||||
|
- **Personal Tags** — user-scoped, private labels (e.g. `hidden gem`, `kid-friendly`).
|
||||||
|
|
||||||
<!-- TODO: screenshot: tag list on place detail -->
|
<!-- TODO: screenshot: tag list on place detail -->
|
||||||
|
|
||||||
@@ -24,6 +26,23 @@ Categories appear in:
|
|||||||
|
|
||||||
> **Admin:** Create and manage categories in [Admin-Categories](Admin-Categories). Only admins can create, edit, or delete categories. All users can read them.
|
> **Admin:** Create and manage categories in [Admin-Categories](Admin-Categories). Only admins can create, edit, or delete categories. All users can read them.
|
||||||
|
|
||||||
|
## Personal Tags
|
||||||
|
|
||||||
|
Tags are private labels owned by each user. They attach to individual places via a many-to-many relationship (`place_tags` table), so the same tag can be applied to as many places as you like, and a single place can carry multiple tags.
|
||||||
|
|
||||||
|
**Fields per tag:**
|
||||||
|
|
||||||
|
- **Name** — free-form text.
|
||||||
|
- **Color** — hex value displayed alongside the tag name. Default: `#10b981` (emerald).
|
||||||
|
|
||||||
|
Tags are scoped to their creator — other trip members do not see your tags, and different users can create tags with identical names without conflict. Deleting a tag automatically removes it from every place it was attached to.
|
||||||
|
|
||||||
|
### Where to manage them
|
||||||
|
|
||||||
|
At the moment tags are exposed primarily through the MCP API — AI assistants connected to your instance can list, create, update, and delete tags (`list_tags`, `create_tag`, `update_tag`, `delete_tag`) and attach them to places through the place endpoints. A dedicated web UI for tag management is not yet available; the filter `tag` parameter on the places API / MCP resource does support filtering places by a tag ID once one exists.
|
||||||
|
|
||||||
|
> **AI / MCP:** See [MCP-Tools-and-Resources](MCP-Tools-and-Resources) for the full tag tool list.
|
||||||
|
|
||||||
## When to use which
|
## When to use which
|
||||||
|
|
||||||
| Use case | Use |
|
| Use case | Use |
|
||||||
|
|||||||
+5
-5
@@ -4,7 +4,7 @@ How to update TREK to a newer version without losing data.
|
|||||||
|
|
||||||
## Before You Update
|
## Before You Update
|
||||||
|
|
||||||
Back up your data first. Go to Admin Panel → Backups and create a manual backup, or copy your `./data` and `./uploads` directories to a safe location. See [Backups] for details.
|
Back up your data first. Go to Admin Panel → Backups and create a manual backup, or copy your `./data` and `./uploads` directories to a safe location. See [Backups](Backups) for details.
|
||||||
|
|
||||||
## Docker Compose (Recommended)
|
## Docker Compose (Recommended)
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ TREK runs any pending database migrations automatically at startup. No manual mi
|
|||||||
|
|
||||||
If you are upgrading from a version that predates the dedicated `ENCRYPTION_KEY` (i.e. you have no `ENCRYPTION_KEY` environment variable set), TREK automatically falls back to `./data/.jwt_secret` on startup and immediately promotes it to `./data/.encryption_key`. No manual steps are required — the transition is handled at first boot after the upgrade.
|
If you are upgrading from a version that predates the dedicated `ENCRYPTION_KEY` (i.e. you have no `ENCRYPTION_KEY` environment variable set), TREK automatically falls back to `./data/.jwt_secret` on startup and immediately promotes it to `./data/.encryption_key`. No manual steps are required — the transition is handled at first boot after the upgrade.
|
||||||
|
|
||||||
If you want to rotate to a new key at any point (not required for a normal update), see [Encryption-Key-Rotation] for the full procedure.
|
If you want to rotate to a new key at any point (not required for a normal update), see [Encryption-Key-Rotation](Encryption-Key-Rotation) for the full procedure.
|
||||||
|
|
||||||
## Unraid
|
## Unraid
|
||||||
|
|
||||||
@@ -50,6 +50,6 @@ In the Unraid Docker tab, click the TREK container and select **Update**. Unraid
|
|||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
- [Backups] — schedule automatic backups so you always have a restore point before updates
|
- [Backups](Backups) — schedule automatic backups so you always have a restore point before updates
|
||||||
- [Encryption-Key-Rotation] — if you need to rotate or migrate the encryption key
|
- [Encryption-Key-Rotation](Encryption-Key-Rotation) — if you need to rotate or migrate the encryption key
|
||||||
- [Install-Docker-Compose] — switch to Compose for easier future updates
|
- [Install-Docker-Compose](Install-Docker-Compose) — switch to Compose for easier future updates
|
||||||
|
|||||||
Reference in New Issue
Block a user