mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f5d01ee09 | |||
| 67ca58f2a5 | |||
| 9eab19ea6c | |||
| 50ea9a74c9 |
@@ -1,6 +1,6 @@
|
||||
import { Hotel, Utensils, ShoppingCart, Bus, Plane, Ticket, Camera, ShoppingBag, FileText, HeartPulse, Coins, MoreHorizontal, Fuel, ParkingCircle } from 'lucide-react'
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import { COST_CATEGORIES, type CostCategory } from '@trek/shared'
|
||||
import { COST_CATEGORIES, costCategoryOf, type CostCategory } from '@trek/shared'
|
||||
|
||||
/**
|
||||
* The fixed Costs categories. Users can't add their own — every expense maps to
|
||||
@@ -34,34 +34,7 @@ export const COST_CAT_META: Record<CostCategory, CostCategoryMeta> = {
|
||||
|
||||
export const COST_CATEGORY_LIST: CostCategoryMeta[] = COST_CATEGORIES.map(k => COST_CAT_META[k])
|
||||
|
||||
/**
|
||||
* Legacy / English free-text categories (and reservation type labels) mapped to
|
||||
* the fixed keys. Bookings used to store labels like "Flight"/"Train"/"Other",
|
||||
* which never matched the lowercase keys and fell through to `other`.
|
||||
*/
|
||||
const LEGACY_CATEGORY_MAP: Record<string, CostCategory> = {
|
||||
flight: 'flights', flights: 'flights', plane: 'flights', flug: 'flights',
|
||||
train: 'transport', bus: 'transport', car: 'transport', 'car rental': 'transport',
|
||||
ferry: 'transport', boat: 'transport', taxi: 'transport', transfer: 'transport', cable_car: 'transport',
|
||||
transport: 'transport', transportation: 'transport',
|
||||
hotel: 'accommodation', accommodation: 'accommodation', lodging: 'accommodation', hostel: 'accommodation',
|
||||
restaurant: 'food', food: 'food', dining: 'food', meal: 'food', meals: 'food',
|
||||
grocery: 'groceries', groceries: 'groceries',
|
||||
activity: 'activities', activities: 'activities',
|
||||
sightseeing: 'sightseeing', sights: 'sightseeing',
|
||||
shop: 'shopping', shopping: 'shopping',
|
||||
fee: 'fees', fees: 'fees',
|
||||
health: 'health', medical: 'health',
|
||||
tip: 'tips', tips: 'tips',
|
||||
gas: 'fuel', fuel: 'fuel', petrol: 'fuel',
|
||||
parking: 'parking', parkings: 'parking', 'car park': 'parking',
|
||||
other: 'other', misc: 'other',
|
||||
}
|
||||
|
||||
/** Map any stored category (incl. legacy/localized free-text values) to a known meta. */
|
||||
export function catMeta(cat: string | null | undefined): CostCategoryMeta {
|
||||
if (!cat) return COST_CAT_META.other
|
||||
if (cat in COST_CAT_META) return COST_CAT_META[cat as CostCategory]
|
||||
const mapped = LEGACY_CATEGORY_MAP[cat.trim().toLowerCase()]
|
||||
return mapped ? COST_CAT_META[mapped] : COST_CAT_META.other
|
||||
return COST_CAT_META[costCategoryOf(cat) ?? 'other']
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"src/db/repositories/Users.repository.ts": 1445,
|
||||
"src/nest/atlas/atlas.service.ts": 1177,
|
||||
"src/nest/auth/auth.service.ts": 1143,
|
||||
"src/nest/budget/budget.service.ts": 1613,
|
||||
"src/nest/budget/budget.service.ts": 1612,
|
||||
"src/nest/collections/collections.service.ts": 1426,
|
||||
"src/nest/doc-sync/doc-sync.service.ts": 1060,
|
||||
"src/nest/doc-sync/providers/synology-drive.client.ts": 1160,
|
||||
|
||||
@@ -26,7 +26,7 @@ import { BudgetWritesService } from './budget-writes.service';
|
||||
import { BudgetService } from './budget.service';
|
||||
import { ExchangeRatesService } from './exchange-rates.service';
|
||||
import { InjectRepository } from '@mikro-orm/nestjs';
|
||||
import { idSchema, sumMinor, toMinor } from '@trek/shared';
|
||||
import { COST_CATEGORY_HINT, idSchema, sumMinor, toMinor } from '@trek/shared';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -200,7 +200,7 @@ export class BudgetMcp {
|
||||
inputSchema: {
|
||||
tripId: idSchema,
|
||||
name: z.string().min(1).max(200),
|
||||
category: z.string().max(100).optional().describe('Budget category (e.g. Accommodation, Food, Transport)'),
|
||||
category: z.string().max(100).optional().describe(COST_CATEGORY_HINT),
|
||||
total_price: z.number().describe('Signed: a negative total records a refund/partial reimbursement (#2176)'),
|
||||
currency: z
|
||||
.string()
|
||||
@@ -341,7 +341,7 @@ export class BudgetMcp {
|
||||
tripId: idSchema,
|
||||
itemId: idSchema,
|
||||
name: z.string().min(1).max(200).optional(),
|
||||
category: z.string().max(100).optional(),
|
||||
category: z.string().max(100).optional().describe(COST_CATEGORY_HINT),
|
||||
total_price: z.number().optional(),
|
||||
currency: z
|
||||
.string()
|
||||
@@ -482,7 +482,7 @@ export class BudgetMcp {
|
||||
inputSchema: {
|
||||
tripId: idSchema,
|
||||
name: z.string().min(1).max(200),
|
||||
category: z.string().max(100).optional().describe('Budget category (e.g. Accommodation, Food, Transport)'),
|
||||
category: z.string().max(100).optional().describe(COST_CATEGORY_HINT),
|
||||
total_price: z.number().describe('Signed: a negative total records a refund/partial reimbursement (#2176)'),
|
||||
note: z.string().max(500).optional(),
|
||||
userIds: z
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@mikro-orm/nestjs';
|
||||
import type { BudgetFallbackFx, BudgetParticipantFinal, BudgetUnconverted, TrekWsPayload, TrekWsTripEventName } from '@trek/shared';
|
||||
import { splitEqualShares, sumMinor, toMinor } from '@trek/shared';
|
||||
import { costCategoryOf, splitEqualShares, storedCostCategory, sumMinor, toMinor } from '@trek/shared';
|
||||
import { RealtimeService } from '../realtime/realtime.service';
|
||||
import { PermissionsService } from '../permissions/permissions.service';
|
||||
import { avatarUrl } from '../common/avatarUrl';
|
||||
@@ -91,8 +91,7 @@ export function splitLegacyTicketNote(
|
||||
* queries instead of a full listSettlements() scan, settlement usernames use
|
||||
* COALESCE(display_name, username) like every item query, and updateMembers no
|
||||
* longer double-applies avatarUrl.
|
||||
*/
|
||||
@Injectable()
|
||||
*/@Injectable()
|
||||
export class BudgetService {
|
||||
constructor(
|
||||
private readonly permissions: PermissionsService,
|
||||
@@ -520,7 +519,7 @@ export class BudgetService {
|
||||
const maxOrder = await this.budgetItemsRepo.maxSortOrder(tripId);
|
||||
const sortOrder = (maxOrder !== null ? maxOrder : -1) + 1;
|
||||
|
||||
const cat = data.category || 'other';
|
||||
const cat = storedCostCategory(data.category);
|
||||
|
||||
// Ensure category has a sort_order entry
|
||||
const catExists = await this.budgetCategoryOrderRepo.exists(tripId, cat);
|
||||
@@ -660,7 +659,7 @@ export class BudgetService {
|
||||
// (the legacy `COALESCE(?, col)` truthy-wins shape), everything else
|
||||
// passes `present = <field> !== undefined` (a true presence sentinel).
|
||||
await this.budgetItemsRepo.update(id, {
|
||||
category: [!!data.category, data.category || ''],
|
||||
category: [!!data.category, data.category ? storedCostCategory(data.category) : ''],
|
||||
name: [!!data.name, data.name || ''],
|
||||
total_price: [data.total_price !== undefined, data.total_price !== undefined ? data.total_price : 0],
|
||||
currency: [data.currency !== undefined, data.currency !== undefined ? (data.currency || null) : null],
|
||||
@@ -705,11 +704,11 @@ export class BudgetService {
|
||||
|
||||
// If category changed, update category order table
|
||||
if (data.category) {
|
||||
const catExists = await this.budgetCategoryOrderRepo.exists(tripId, data.category);
|
||||
const catExists = await this.budgetCategoryOrderRepo.exists(tripId, storedCostCategory(data.category));
|
||||
if (!catExists) {
|
||||
const maxCatOrder = await this.budgetCategoryOrderRepo.maxSortOrder(tripId);
|
||||
const catOrder = (maxCatOrder !== null && maxCatOrder !== undefined ? maxCatOrder : -1) + 1;
|
||||
await this.budgetCategoryOrderRepo.insertIgnore(tripId, data.category, catOrder);
|
||||
await this.budgetCategoryOrderRepo.insertIgnore(tripId, storedCostCategory(data.category), catOrder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1037,7 +1036,7 @@ export class BudgetService {
|
||||
}
|
||||
const cents = toMinor(convert(r.total_price || 0));
|
||||
total += cents;
|
||||
const cat = r.category || '';
|
||||
const cat = costCategoryOf(r.category) ?? (r.category || '');
|
||||
byCategory[cat] = (byCategory[cat] || 0) + cents;
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -383,7 +383,7 @@ export class JourneyMcp {
|
||||
tags: z.array(z.string()).optional(),
|
||||
pros_cons: PROS_CONS.optional().describe('The verdict on the place: what was worth it and what was not'),
|
||||
visibility: ENTRY_VISIBILITY.optional().describe(
|
||||
'Defaults to private; "shared" and "public" expose the entry through the journey share link',
|
||||
'Stored with the entry, defaults to private. It does not decide the share link: every entry that is not a draft shows on the journey share link. Use is_draft to keep an entry to the contributors',
|
||||
),
|
||||
type: ENTRY_TYPE.optional().describe(
|
||||
'Defaults to "entry"; "skeleton" is the stub TREK derives from a trip place and hides behind the hide-skeletons preference',
|
||||
|
||||
@@ -155,7 +155,8 @@ describe('Tool: create_budget_item', () => {
|
||||
});
|
||||
const data = parseToolResult(result) as any;
|
||||
expect(data.item.name).toBe('Hotel Paris');
|
||||
expect(data.item.category).toBe('Accommodation');
|
||||
// Stored as its fixed key, as the Costs tab shows it (#2835).
|
||||
expect(data.item.category).toBe('accommodation');
|
||||
expect(data.item.total_price).toBe(500);
|
||||
expect(data.item.note).toBe('Prepaid');
|
||||
});
|
||||
|
||||
@@ -565,10 +565,10 @@ describe('Prompt: budget-overview', () => {
|
||||
const client = await buildServer(user.id);
|
||||
const text = await invokePromptText(client, 'budget-overview', { tripId: trip.id });
|
||||
expect(text).toContain('Budget Trip');
|
||||
expect(text).toContain('Transport');
|
||||
expect(text).toContain('Accommodation');
|
||||
expect(text).toContain('550'); // Transport total
|
||||
expect(text).toContain('300'); // Accommodation total
|
||||
// Totals group by the fixed Costs key, whatever case the expense was stored in (#2835).
|
||||
expect(text).toContain('- transport: 250');
|
||||
expect(text).toContain('- accommodation: 300');
|
||||
expect(text).toContain('550');
|
||||
});
|
||||
|
||||
it('prints a foreign-currency bill at the rate it was booked at, not as that many euros (#2525)', async () => {
|
||||
@@ -588,8 +588,8 @@ describe('Prompt: budget-overview', () => {
|
||||
const text = await invokePromptText(client, 'budget-overview', { tripId: trip.id });
|
||||
// 801.76 USD at 1.17 is 685.26 EUR. Summed as stored it read "Total: 901.76 EUR".
|
||||
expect(text).toContain('Total: 785.26 EUR');
|
||||
expect(text).toContain('- Accommodation: 685.26 EUR');
|
||||
expect(text).toContain('- Food: 100 EUR');
|
||||
expect(text).toContain('- accommodation: 685.26 EUR');
|
||||
expect(text).toContain('- food: 100 EUR');
|
||||
expect(text).not.toContain('801.76');
|
||||
});
|
||||
|
||||
|
||||
@@ -291,7 +291,7 @@ describe('MCP transit tools', () => {
|
||||
arguments: { tripId: trip.id, dayId: day.id, from, to, itinerary, price: 230, budget_category: 'Transport' },
|
||||
}),
|
||||
) as { reservation: { id: number; metadata: string } };
|
||||
expect((await findRow(orm, BudgetItems, { reservation: paid.reservation.id }))?.category).toBe('Transport');
|
||||
expect((await findRow(orm, BudgetItems, { reservation: paid.reservation.id }))?.category).toBe('transport');
|
||||
|
||||
const free = parseToolResult(
|
||||
await harness.client.callTool({
|
||||
|
||||
@@ -349,7 +349,8 @@ describe('Transport tools: the price link', () => {
|
||||
const data = parseToolResult(result) as { reservation: { id: number } };
|
||||
|
||||
const item = await findRow(orm, BudgetItems, { reservation: data.reservation.id });
|
||||
expect(item).toMatchObject({ name: 'ZRH → CDG', category: 'Flights', total_price: 240 });
|
||||
// Stored as its fixed Costs key (#2835).
|
||||
expect(item).toMatchObject({ name: 'ZRH → CDG', category: 'flights', total_price: 240 });
|
||||
// The price also rides along in the reservation metadata, as the REST path does.
|
||||
expect(broadcastMock.mock.calls.some((c) => c[1] === 'budget:created')).toBe(true);
|
||||
});
|
||||
@@ -365,7 +366,8 @@ describe('Transport tools: the price link', () => {
|
||||
});
|
||||
const data = parseToolResult(result) as { reservation: { id: number } };
|
||||
const item = await findRow(orm, BudgetItems, { reservation: data.reservation.id });
|
||||
expect(item?.category).toBe('train');
|
||||
// The type is stored as its Costs key: a train is a transport (#2835).
|
||||
expect(item?.category).toBe('transport');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// COST-CATEGORY-001 to COST-CATEGORY-005: a typed category read as one of the fixed keys.
|
||||
import { COST_CATEGORY_HINT, costCategoryOf, storedCostCategory } from './cost-category';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('costCategoryOf', () => {
|
||||
it('COST-CATEGORY-001: a key stays itself', () => {
|
||||
expect(costCategoryOf('food')).toBe('food');
|
||||
expect(costCategoryOf('accommodation')).toBe('accommodation');
|
||||
});
|
||||
|
||||
it('COST-CATEGORY-002: another case or a common name reads as its key', () => {
|
||||
expect(costCategoryOf('Food')).toBe('food');
|
||||
expect(costCategoryOf(' FOOD ')).toBe('food');
|
||||
expect(costCategoryOf('Flight')).toBe('flights');
|
||||
expect(costCategoryOf('Hotel')).toBe('accommodation');
|
||||
expect(costCategoryOf('Car park')).toBe('parking');
|
||||
});
|
||||
|
||||
it('COST-CATEGORY-003: anything else is no key', () => {
|
||||
expect(costCategoryOf('Comida')).toBeNull();
|
||||
expect(costCategoryOf('')).toBeNull();
|
||||
expect(costCategoryOf(null)).toBeNull();
|
||||
expect(costCategoryOf(undefined)).toBeNull();
|
||||
});
|
||||
|
||||
it('COST-CATEGORY-004: an expense is stored under the key, other text as given, nothing as other', () => {
|
||||
expect(storedCostCategory('Food')).toBe('food');
|
||||
expect(storedCostCategory('Train')).toBe('transport');
|
||||
expect(storedCostCategory('Comida')).toBe('Comida');
|
||||
expect(storedCostCategory('')).toBe('other');
|
||||
expect(storedCostCategory(null)).toBe('other');
|
||||
expect(storedCostCategory(undefined)).toBe('other');
|
||||
});
|
||||
|
||||
it('COST-CATEGORY-005: the hint an assistant reads names every key', () => {
|
||||
for (const key of ['accommodation', 'food', 'transport', 'flights', 'other'])
|
||||
expect(COST_CATEGORY_HINT).toContain(key);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import { COST_CATEGORIES, type CostCategory } from './budget.schema';
|
||||
|
||||
/**
|
||||
* Free-text and legacy categories (and booking type labels) mapped to the fixed
|
||||
* keys. Bookings used to store labels like "Flight"/"Train"/"Other", and an
|
||||
* assistant writing through the MCP says "Food" where the key is "food".
|
||||
*/
|
||||
const CATEGORY_ALIASES: Record<string, CostCategory> = {
|
||||
flight: 'flights',
|
||||
flights: 'flights',
|
||||
plane: 'flights',
|
||||
flug: 'flights',
|
||||
train: 'transport',
|
||||
bus: 'transport',
|
||||
car: 'transport',
|
||||
'car rental': 'transport',
|
||||
ferry: 'transport',
|
||||
boat: 'transport',
|
||||
taxi: 'transport',
|
||||
transfer: 'transport',
|
||||
cable_car: 'transport',
|
||||
transport: 'transport',
|
||||
transportation: 'transport',
|
||||
hotel: 'accommodation',
|
||||
accommodation: 'accommodation',
|
||||
lodging: 'accommodation',
|
||||
hostel: 'accommodation',
|
||||
restaurant: 'food',
|
||||
food: 'food',
|
||||
dining: 'food',
|
||||
meal: 'food',
|
||||
meals: 'food',
|
||||
grocery: 'groceries',
|
||||
groceries: 'groceries',
|
||||
activity: 'activities',
|
||||
activities: 'activities',
|
||||
sightseeing: 'sightseeing',
|
||||
sights: 'sightseeing',
|
||||
shop: 'shopping',
|
||||
shopping: 'shopping',
|
||||
fee: 'fees',
|
||||
fees: 'fees',
|
||||
health: 'health',
|
||||
medical: 'health',
|
||||
tip: 'tips',
|
||||
tips: 'tips',
|
||||
gas: 'fuel',
|
||||
fuel: 'fuel',
|
||||
petrol: 'fuel',
|
||||
parking: 'parking',
|
||||
parkings: 'parking',
|
||||
'car park': 'parking',
|
||||
other: 'other',
|
||||
misc: 'other',
|
||||
};
|
||||
|
||||
const KEYS: ReadonlySet<string> = new Set(COST_CATEGORIES);
|
||||
|
||||
/**
|
||||
* The fixed key a stored or typed category stands for, whatever its case, or
|
||||
* null for a category that is none of them.
|
||||
*/
|
||||
export function costCategoryOf(category: string | null | undefined): CostCategory | null {
|
||||
if (!category) return null;
|
||||
if (KEYS.has(category)) return category as CostCategory;
|
||||
return CATEGORY_ALIASES[category.trim().toLowerCase()] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The category an expense is stored under: the fixed key when the text names one,
|
||||
* so "Food" and "food" are one category, and the text itself otherwise.
|
||||
*/
|
||||
export function storedCostCategory(category: string | null | undefined): string {
|
||||
return costCategoryOf(category) ?? (category || 'other');
|
||||
}
|
||||
|
||||
/** What the MCP tells an assistant about the category field. */
|
||||
export const COST_CATEGORY_HINT = `One of the Costs categories: ${COST_CATEGORIES.join(', ')}. Another case or a common name (Hotel, Flight, Food) is stored as its key; any other text is kept as given and shows as other`;
|
||||
@@ -38,6 +38,7 @@ export * from './packing/packed-count';
|
||||
export * from './packing/packing-placeholder';
|
||||
export * from './todo/todo.schema';
|
||||
export * from './budget/budget.schema';
|
||||
export * from './budget/cost-category';
|
||||
export * from './budget/receipt-scan.schema';
|
||||
export * from './reservation/reservation.schema';
|
||||
export * from './reservation/reservation-types';
|
||||
|
||||
@@ -293,7 +293,11 @@ export const journeyEntryUpdateToolSchema = z.strictObject({
|
||||
weather: z.string().max(100).nullable().optional(),
|
||||
tags: z.array(z.string()).nullable().optional(),
|
||||
pros_cons: journeyEntryProsConsSchema.nullable().optional().describe('Replaces the whole verdict; null clears it'),
|
||||
visibility: journeyEntryVisibilitySchema.optional(),
|
||||
visibility: journeyEntryVisibilitySchema
|
||||
.optional()
|
||||
.describe(
|
||||
'Stored with the entry. It does not decide the share link: every entry that is not a draft shows there. Use is_draft to keep an entry to the contributors',
|
||||
),
|
||||
type: journeyEntryTypeSchema
|
||||
.optional()
|
||||
.describe('Promote a trip-derived "skeleton" to a real "entry" once it has been written up'),
|
||||
|
||||
@@ -42,6 +42,8 @@ Each entry corresponds to a day in your journey. The entry editor provides:
|
||||
- **Weather** — choose one of six values: Sunny, Partly cloudy, Cloudy, Rainy, Stormy, Snowy. (Snowy is stored under the id `cold`.)
|
||||
- **Photos and videos** — attach photos to the entry. The first photo becomes the card thumbnail in list views. A video clip goes on an entry the same way: `mp4`, `m4v`, `webm` or `mov`, up to 500 MB per file. The clip is stored as uploaded, without transcoding, and the editor shows its first frame as a preview before you save. The journey gallery takes the same clips.
|
||||
> **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.).
|
||||
|
||||
> **Note on Android:** the photo picker of Android browsers removes the location from photos before they reach any website, so a photo uploaded from an Android phone usually arrives without GPS and stays off the map. Photos added from a connected Immich or Synology Photos library keep their location, because TREK reads it from the library instead of the uploaded file.
|
||||
- **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.
|
||||
- **Tags** — free-form labels (e.g. "hidden gem", "best meal").
|
||||
- **Location** — pin the entry to a map location.
|
||||
|
||||
@@ -199,7 +199,7 @@ Requires `journey:read` or `journey:write` scope.
|
||||
| `list_journey_entries` | List the entries of a journey in reading order (date, text, mood, linked trip), with the journey's id and title. `from` and `to` (`YYYY-MM-DD`, both included) keep only the entries of those days. |
|
||||
| `get_journey_entry` | One journal entry by its id, as `list_journey_entries` answers it (photos included), with the id and title of its journey. |
|
||||
| `create_journey_entry` | Add an entry with date (required), optional title, story text, time of day, location name, mood, and sort order. `is_draft: true` creates it as a draft that the journey share link leaves out. |
|
||||
| `update_journey_entry` | Edit a journey entry's title, story, date, time of day, place, coordinates, weather, tags, mood, pros/cons list or visibility. `stats_excluded: true` keeps the entry but takes it off the route and out of `get_journey_stats`; `dismissed: true` waves a trip-derived suggestion away without deleting it, so the trip sync does not offer it again. `is_draft` turns the entry into a draft or publishes it. Answers with what the changed fields held before under `previous`. |
|
||||
| `update_journey_entry` | Edit a journey entry's title, story, date, time of day, place, coordinates, weather, tags, mood, pros/cons list or visibility. `stats_excluded: true` keeps the entry but takes it off the route and out of `get_journey_stats`; `dismissed: true` waves a trip-derived suggestion away without deleting it, so the trip sync does not offer it again. `is_draft` turns the entry into a draft or publishes it; `visibility` is stored with the entry but keeps nothing off the share link, only a draft stays off it. Answers with what the changed fields held before under `previous`. |
|
||||
| `delete_journey_entry` | Remove an entry from a journey. |
|
||||
| `reorder_journey_entries` | Reorder entries by providing the new ordered list of entry IDs. Answers with the ids in their order before under `previous_order`. |
|
||||
| `reorder_journey_entry_photos` | Put the photos of one entry in a new order by passing every photo id of the entry once. The first photo is the entry's cover. |
|
||||
|
||||
@@ -347,6 +347,26 @@ docker logs <container> 2>&1 | grep -E "SMTP test email (sent|failed)|SMTP test
|
||||
|
||||
---
|
||||
|
||||
## My location does not work: "Location requires a secure (HTTPS) connection"
|
||||
|
||||
**Cause:** Browsers only hand out the device location in a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts). Over plain HTTP at an address other than `localhost`, the location button on the map shows *Location requires a secure (HTTPS) connection*, and the journal's current location stays empty. The same goes for a phone opening TREK by its LAN address.
|
||||
|
||||
**Fix:** Open TREK over HTTPS. Two common ways when TREK runs at home:
|
||||
|
||||
- **A reverse proxy with a certificate**, see [Reverse-Proxy](Reverse-Proxy).
|
||||
- **An HTTPS tunnel** (Cloudflare Tunnel, Tailscale Serve, ngrok and the like) that ends at TREK's HTTP port.
|
||||
|
||||
With a tunnel or proxy in front and plain HTTP still used on the LAN, set:
|
||||
|
||||
- `APP_URL` to the HTTPS address, so links in emails and calendar feeds use it.
|
||||
- `TRUST_PROXY=1`, so TREK sees that the request came in over HTTPS.
|
||||
- `COOKIE_SECURE=false` only if you also sign in over plain HTTP on the LAN; without it, that sign-in keeps no session. HTTPS sign-ins work either way.
|
||||
- `ALLOWED_ORIGINS` needs no entry for either address: the address in the browser's address bar is always allowed. Add origins only for other sites that call TREK.
|
||||
|
||||
See [Environment Variables](Environment-Variables) for each of them.
|
||||
|
||||
---
|
||||
|
||||
## CORS error — API requests blocked in the browser
|
||||
|
||||
**Cause:** If `ALLOWED_ORIGINS` is set, only those origins are permitted, plus the host the request was sent to (the address in the browser's address bar is never cross-origin to itself). A request from any other origin is refused with `403 Not allowed by CORS`, and the server logs a `CORS: refused origin ...` warning naming the origin to add.
|
||||
|
||||
Reference in New Issue
Block a user