From 93c0d6fe785a832136553277728b173e43af9523 Mon Sep 17 00:00:00 2001 From: jubnl Date: Fri, 3 Apr 2026 23:58:15 +0200 Subject: [PATCH 1/7] fix(trips): default to 7-day window when dates are omitted on creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - No dates → tomorrow to tomorrow+7d - Start only → end = start+7d - End only → start = end-7d - Both provided → unchanged fix(ci): include client/package-lock.json in version bump commit --- .github/workflows/docker.yml | 2 +- server/src/routes/trips.ts | 21 +++++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 27bab6f5..326db6c1 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -58,7 +58,7 @@ jobs: # 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 + git add server/package.json server/package-lock.json client/package.json client/package-lock.json 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/server/src/routes/trips.ts b/server/src/routes/trips.ts index b35fb1a7..c0f82674 100644 --- a/server/src/routes/trips.ts +++ b/server/src/routes/trips.ts @@ -74,9 +74,26 @@ router.post('/', authenticate, (req: Request, res: Response) => { if (!checkPermission('trip_create', authReq.user.role, null, authReq.user.id, false)) return res.status(403).json({ error: 'No permission to create trips' }); - const { title, description, start_date, end_date, currency, reminder_days } = req.body; + const { title, description, currency, reminder_days } = req.body; if (!title) return res.status(400).json({ error: 'Title is required' }); - if (start_date && end_date && new Date(end_date) < new Date(start_date)) + + const toDateStr = (d: Date) => d.toISOString().slice(0, 10); + const addDays = (d: Date, n: number) => { const r = new Date(d); r.setDate(r.getDate() + n); return r; }; + + let start_date: string | null = req.body.start_date || null; + let end_date: string | null = req.body.end_date || null; + + if (!start_date && !end_date) { + const tomorrow = addDays(new Date(), 1); + start_date = toDateStr(tomorrow); + end_date = toDateStr(addDays(tomorrow, 7)); + } else if (start_date && !end_date) { + end_date = toDateStr(addDays(new Date(start_date), 7)); + } else if (!start_date && end_date) { + start_date = toDateStr(addDays(new Date(end_date), -7)); + } + + if (new Date(end_date!) < new Date(start_date!)) return res.status(400).json({ error: 'End date must be after start date' }); const { trip, tripId, reminderDays } = createTrip(authReq.user.id, { title, description, start_date, end_date, currency, reminder_days }); From 6400c2d27d2554d2165b2c2de4acc21a5d146fc8 Mon Sep 17 00:00:00 2001 From: jubnl Date: Sat, 4 Apr 2026 00:09:28 +0200 Subject: [PATCH 2/7] fix(mcp): wire check_in/check_out times through hotel accommodation tools Adds optional check_in and check_out fields to create_reservation and link_hotel_accommodation so MCP clients can set accommodation times, matching the existing REST API behaviour. Closes #363 --- server/src/mcp/tools.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/server/src/mcp/tools.ts b/server/src/mcp/tools.ts index fd3180c2..e1479078 100644 --- a/server/src/mcp/tools.ts +++ b/server/src/mcp/tools.ts @@ -509,10 +509,12 @@ export function registerTools(server: McpServer, userId: number): void { place_id: z.number().int().positive().optional().describe('Hotel place to link (hotel type only)'), start_day_id: z.number().int().positive().optional().describe('Check-in day (hotel type only; requires place_id and end_day_id)'), end_day_id: z.number().int().positive().optional().describe('Check-out day (hotel type only; requires place_id and start_day_id)'), + check_in: z.string().max(10).optional().describe('Check-in time (e.g. "15:00", hotel type only)'), + check_out: z.string().max(10).optional().describe('Check-out time (e.g. "11:00", hotel type only)'), assignment_id: z.number().int().positive().optional().describe('Link to a day assignment (restaurant, train, car, cruise, event, tour, activity, other)'), }, }, - async ({ tripId, title, type, reservation_time, location, confirmation_number, notes, day_id, place_id, start_day_id, end_day_id, assignment_id }) => { + async ({ tripId, title, type, reservation_time, location, confirmation_number, notes, day_id, place_id, start_day_id, end_day_id, check_in, check_out, assignment_id }) => { if (isDemoUser(userId)) return demoDenied(); if (!canAccessTrip(tripId, userId)) return noAccess(); @@ -542,8 +544,8 @@ export function registerTools(server: McpServer, userId: number): void { let accommodationId: number | null = null; if (type === 'hotel' && place_id && start_day_id && end_day_id) { const accResult = db.prepare( - 'INSERT INTO day_accommodations (trip_id, place_id, start_day_id, end_day_id, confirmation) VALUES (?, ?, ?, ?, ?)' - ).run(tripId, place_id, start_day_id, end_day_id, confirmation_number || null); + 'INSERT INTO day_accommodations (trip_id, place_id, start_day_id, end_day_id, check_in, check_out, confirmation) VALUES (?, ?, ?, ?, ?, ?, ?)' + ).run(tripId, place_id, start_day_id, end_day_id, check_in || null, check_out || null, confirmation_number || null); accommodationId = accResult.lastInsertRowid as number; } const result = db.prepare(` @@ -599,9 +601,11 @@ export function registerTools(server: McpServer, userId: number): void { place_id: z.number().int().positive().describe('The hotel place to link'), start_day_id: z.number().int().positive().describe('Check-in day ID'), end_day_id: z.number().int().positive().describe('Check-out day ID'), + check_in: z.string().max(10).optional().describe('Check-in time (e.g. "15:00")'), + check_out: z.string().max(10).optional().describe('Check-out time (e.g. "11:00")'), }, }, - async ({ tripId, reservationId, place_id, start_day_id, end_day_id }) => { + async ({ tripId, reservationId, place_id, start_day_id, end_day_id, check_in, check_out }) => { if (isDemoUser(userId)) return demoDenied(); if (!canAccessTrip(tripId, userId)) return noAccess(); const reservation = db.prepare('SELECT * FROM reservations WHERE id = ? AND trip_id = ?').get(reservationId, tripId) as Record | undefined; @@ -619,12 +623,12 @@ export function registerTools(server: McpServer, userId: number): void { const isNewAccommodation = !accommodationId; db.transaction(() => { if (accommodationId) { - db.prepare('UPDATE day_accommodations SET place_id = ?, start_day_id = ?, end_day_id = ? WHERE id = ?') - .run(place_id, start_day_id, end_day_id, accommodationId); + db.prepare('UPDATE day_accommodations SET place_id = ?, start_day_id = ?, end_day_id = ?, check_in = COALESCE(?, check_in), check_out = COALESCE(?, check_out) WHERE id = ?') + .run(place_id, start_day_id, end_day_id, check_in || null, check_out || null, accommodationId); } else { const accResult = db.prepare( - 'INSERT INTO day_accommodations (trip_id, place_id, start_day_id, end_day_id, confirmation) VALUES (?, ?, ?, ?, ?)' - ).run(tripId, place_id, start_day_id, end_day_id, reservation.confirmation_number || null); + 'INSERT INTO day_accommodations (trip_id, place_id, start_day_id, end_day_id, check_in, check_out, confirmation) VALUES (?, ?, ?, ?, ?, ?, ?)' + ).run(tripId, place_id, start_day_id, end_day_id, check_in || null, check_out || null, reservation.confirmation_number || null); accommodationId = accResult.lastInsertRowid as number; } db.prepare('UPDATE reservations SET place_id = ?, accommodation_id = ? WHERE id = ?') From ae0d48ac83e1e15bfb117904cd0fecd73232145a Mon Sep 17 00:00:00 2001 From: jubnl Date: Sat, 4 Apr 2026 00:14:11 +0200 Subject: [PATCH 3/7] fix(immich): check all trips when verifying shared photo access canAccessUserPhoto was using .get() which only returned the first matching trip, causing access to be incorrectly denied when a photo was shared across multiple trips and the requester was a member of a non-first trip. --- server/src/services/immichService.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/services/immichService.ts b/server/src/services/immichService.ts index aceb8c00..04290868 100644 --- a/server/src/services/immichService.ts +++ b/server/src/services/immichService.ts @@ -236,12 +236,12 @@ export function togglePhotoSharing(tripId: string, userId: number, assetId: stri * the same trip that contains the photo. */ export function canAccessUserPhoto(requestingUserId: number, ownerUserId: number, assetId: string): boolean { - const row = db.prepare(` + const rows = db.prepare(` SELECT tp.trip_id FROM trip_photos tp WHERE tp.immich_asset_id = ? AND tp.user_id = ? AND tp.shared = 1 - `).get(assetId, ownerUserId) as { trip_id: number } | undefined; - if (!row) return false; - return !!canAccessTrip(String(row.trip_id), requestingUserId); + `).all(assetId, ownerUserId) as { trip_id: number }[]; + if (rows.length === 0) return false; + return rows.some(row => !!canAccessTrip(String(row.trip_id), requestingUserId)); } export async function getAssetInfo( From a307d8d1c98044b6e52e680989087196403dad6c Mon Sep 17 00:00:00 2001 From: jubnl Date: Sat, 4 Apr 2026 00:17:34 +0200 Subject: [PATCH 4/7] test(trips): update TRIP-002 to expect default 7-day window Now that trips always default to a start+7 day window when no dates are provided, the test expectation of null dates and zero dated days is no longer valid. --- server/tests/integration/trips.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/tests/integration/trips.test.ts b/server/tests/integration/trips.test.ts index 6c619d73..b862b237 100644 --- a/server/tests/integration/trips.test.ts +++ b/server/tests/integration/trips.test.ts @@ -89,7 +89,7 @@ describe('Create trip', () => { expect(days[4].date).toBe('2026-06-05'); }); - it('TRIP-002 — POST /api/trips without dates returns 201 and no date-specific days', async () => { + it('TRIP-002 — POST /api/trips without dates returns 201 and defaults to a 7-day window', async () => { const { user } = createUser(testDb); const res = await request(app) @@ -99,12 +99,12 @@ describe('Create trip', () => { expect(res.status).toBe(201); expect(res.body.trip).toBeDefined(); - expect(res.body.trip.start_date).toBeNull(); - expect(res.body.trip.end_date).toBeNull(); + expect(res.body.trip.start_date).not.toBeNull(); + expect(res.body.trip.end_date).not.toBeNull(); - // Days with explicit dates should not be present + // Should have 8 days (start + 7 day window) const daysWithDate = testDb.prepare('SELECT * FROM days WHERE trip_id = ? AND date IS NOT NULL').all(res.body.trip.id) as any[]; - expect(daysWithDate).toHaveLength(0); + expect(daysWithDate).toHaveLength(8); }); it('TRIP-001 — POST /api/trips requires a title, returns 400 without one', async () => { From 846db9d076ec0d72eb79c30d7db6d93b69918a1b Mon Sep 17 00:00:00 2001 From: jubnl Date: Sat, 4 Apr 2026 00:18:13 +0200 Subject: [PATCH 5/7] test(trips): assert exact start/end dates in TRIP-002 Replace not-null checks with exact date assertions mirroring the route's defaulting logic (tomorrow + 7-day window). --- server/tests/integration/trips.test.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/server/tests/integration/trips.test.ts b/server/tests/integration/trips.test.ts index b862b237..16d224d8 100644 --- a/server/tests/integration/trips.test.ts +++ b/server/tests/integration/trips.test.ts @@ -92,6 +92,12 @@ describe('Create trip', () => { it('TRIP-002 — POST /api/trips without dates returns 201 and defaults to a 7-day window', async () => { const { user } = createUser(testDb); + const addDays = (d: Date, n: number) => { const r = new Date(d); r.setDate(r.getDate() + n); return r; }; + const toDateStr = (d: Date) => d.toISOString().slice(0, 10); + const tomorrow = addDays(new Date(), 1); + const expectedStart = toDateStr(tomorrow); + const expectedEnd = toDateStr(addDays(tomorrow, 7)); + const res = await request(app) .post('/api/trips') .set('Cookie', authCookie(user.id)) @@ -99,10 +105,10 @@ describe('Create trip', () => { expect(res.status).toBe(201); expect(res.body.trip).toBeDefined(); - expect(res.body.trip.start_date).not.toBeNull(); - expect(res.body.trip.end_date).not.toBeNull(); + expect(res.body.trip.start_date).toBe(expectedStart); + expect(res.body.trip.end_date).toBe(expectedEnd); - // Should have 8 days (start + 7 day window) + // Should have 8 days (start through end inclusive) const daysWithDate = testDb.prepare('SELECT * FROM days WHERE trip_id = ? AND date IS NOT NULL').all(res.body.trip.id) as any[]; expect(daysWithDate).toHaveLength(8); }); From 2197e0e1fd2eeafd20d12b90a72b7f37741d6fa4 Mon Sep 17 00:00:00 2001 From: jubnl Date: Sat, 4 Apr 2026 00:19:23 +0200 Subject: [PATCH 6/7] ci(test): remove push trigger, keep only pull_request --- .github/workflows/test.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fb161122..70eea819 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,11 +4,6 @@ permissions: contents: read on: - push: - branches: [main, dev] - paths: - - 'server/**' - - '.github/workflows/test.yml' pull_request: branches: [main, dev] paths: From 2469739bcaa5fb431745370f5671d52ff364ef72 Mon Sep 17 00:00:00 2001 From: jubnl Date: Sat, 4 Apr 2026 00:21:01 +0200 Subject: [PATCH 7/7] fix(admin): update stale NOMAD references to TREK - GitHubPanel: point release fetcher to mauriceboe/TREK - AdminPage: fix Docker update instructions (image, container name, volume paths) - es.ts: replace all remaining NOMAD occurrences with TREK --- client/src/components/Admin/GitHubPanel.tsx | 2 +- client/src/i18n/translations/es.ts | 18 +++++++++--------- client/src/pages/AdminPage.tsx | 12 ++++++------ 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/client/src/components/Admin/GitHubPanel.tsx b/client/src/components/Admin/GitHubPanel.tsx index 96a3b286..64b469ac 100644 --- a/client/src/components/Admin/GitHubPanel.tsx +++ b/client/src/components/Admin/GitHubPanel.tsx @@ -3,7 +3,7 @@ import { Tag, Calendar, ExternalLink, ChevronDown, ChevronUp, Loader2, Heart, Co import { getLocaleForLanguage, useTranslation } from '../../i18n' import apiClient from '../../api/client' -const REPO = 'mauriceboe/NOMAD' +const REPO = 'mauriceboe/TREK' const PER_PAGE = 10 export default function GitHubPanel() { diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index b1142fd2..4de28e2c 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -327,7 +327,7 @@ const es: Record = { 'login.signingIn': 'Iniciando sesión…', 'login.signIn': 'Entrar', 'login.createAdmin': 'Crear cuenta de administrador', - 'login.createAdminHint': 'Configura la primera cuenta administradora de NOMAD.', + 'login.createAdminHint': 'Configura la primera cuenta administradora de TREK.', 'login.setNewPassword': 'Establecer nueva contraseña', 'login.setNewPasswordHint': 'Debe cambiar su contraseña antes de continuar.', 'login.createAccount': 'Crear cuenta', @@ -483,7 +483,7 @@ const es: Record = { // Addons 'admin.tabs.addons': 'Complementos', 'admin.addons.title': 'Complementos', - 'admin.addons.subtitle': 'Activa o desactiva funciones para personalizar tu experiencia en NOMAD.', + 'admin.addons.subtitle': 'Activa o desactiva funciones para personalizar tu experiencia en TREK.', 'admin.addons.subtitleBefore': 'Activa o desactiva funciones para personalizar tu experiencia en ', 'admin.addons.subtitleAfter': '.', 'admin.addons.enabled': 'Activo', @@ -499,7 +499,7 @@ const es: Record = { 'admin.addons.noAddons': 'No hay complementos disponibles', 'admin.weather.title': 'Datos meteorológicos', 'admin.weather.badge': 'Desde el 24 de marzo de 2026', - 'admin.weather.description': 'NOMAD utiliza Open-Meteo como fuente de datos meteorológicos. Open-Meteo es un servicio meteorológico gratuito y de código abierto: no requiere clave API.', + 'admin.weather.description': 'TREK utiliza Open-Meteo como fuente de datos meteorológicos. Open-Meteo es un servicio meteorológico gratuito y de código abierto: no requiere clave API.', 'admin.weather.forecast': 'Pronóstico de 16 días', 'admin.weather.forecastDesc': 'Antes eran 5 días (OpenWeatherMap)', 'admin.weather.climate': 'Datos climáticos históricos', @@ -551,11 +551,11 @@ const es: Record = { 'admin.github.error': 'No se pudieron cargar las versiones', 'admin.github.by': 'por', 'admin.update.available': 'Actualización disponible', - 'admin.update.text': 'NOMAD {version} está disponible. Estás usando {current}.', + 'admin.update.text': 'TREK {version} está disponible. Estás usando {current}.', 'admin.update.button': 'Ver en GitHub', 'admin.update.install': 'Instalar actualización', 'admin.update.confirmTitle': '¿Instalar actualización?', - 'admin.update.confirmText': 'NOMAD se actualizará de {current} a {version}. Después, el servidor se reiniciará automáticamente.', + 'admin.update.confirmText': 'TREK se actualizará de {current} a {version}. Después, el servidor se reiniciará automáticamente.', 'admin.update.dataInfo': 'Todos tus datos (viajes, usuarios, claves API, subidas, Vacay, Atlas, presupuestos) se conservarán.', 'admin.update.warning': 'La app estará brevemente no disponible durante el reinicio.', 'admin.update.confirm': 'Actualizar ahora', @@ -565,7 +565,7 @@ const es: Record = { 'admin.update.backupHint': 'Recomendamos crear una copia de seguridad antes de actualizar.', 'admin.update.backupLink': 'Ir a Copia de seguridad', 'admin.update.howTo': 'Cómo actualizar', - 'admin.update.dockerText': 'Tu instancia de NOMAD se ejecuta en Docker. Para actualizar a {version}, ejecuta los siguientes comandos en tu servidor:', + 'admin.update.dockerText': 'Tu instancia de TREK se ejecuta en Docker. Para actualizar a {version}, ejecuta los siguientes comandos en tu servidor:', 'admin.update.reloadHint': 'Recarga la página en unos segundos.', // Vacay addon @@ -620,9 +620,9 @@ const es: Record = { 'vacay.carryOver': 'Arrastrar saldo', 'vacay.carryOverHint': 'Trasladar automáticamente los días restantes al año siguiente', 'vacay.sharing': 'Compartir', - 'vacay.sharingHint': 'Comparte tu calendario de vacaciones con otros usuarios de NOMAD', + 'vacay.sharingHint': 'Comparte tu calendario de vacaciones con otros usuarios de TREK', 'vacay.owner': 'Propietario', - 'vacay.shareEmailPlaceholder': 'Correo electrónico del usuario de NOMAD', + 'vacay.shareEmailPlaceholder': 'Correo electrónico del usuario de TREK', 'vacay.shareSuccess': 'Plan compartido correctamente', 'vacay.shareError': 'No se pudo compartir el plan', 'vacay.dissolve': 'Deshacer fusión', @@ -634,7 +634,7 @@ const es: Record = { 'vacay.noData': 'Sin datos', 'vacay.changeColor': 'Cambiar color', 'vacay.inviteUser': 'Invitar usuario', - 'vacay.inviteHint': 'Invita a otro usuario de NOMAD a compartir un calendario combinado de vacaciones.', + 'vacay.inviteHint': 'Invita a otro usuario de TREK a compartir un calendario combinado de vacaciones.', 'vacay.selectUser': 'Seleccionar usuario', 'vacay.sendInvite': 'Enviar invitación', 'vacay.inviteSent': 'Invitación enviada', diff --git a/client/src/pages/AdminPage.tsx b/client/src/pages/AdminPage.tsx index 45ef2b01..471338c9 100644 --- a/client/src/pages/AdminPage.tsx +++ b/client/src/pages/AdminPage.tsx @@ -1358,14 +1358,14 @@ export default function AdminPage(): React.ReactElement {
-{`docker pull mauriceboe/nomad:latest -docker stop nomad && docker rm nomad -docker run -d --name nomad \\ +{`docker pull mauriceboe/trek:latest +docker stop trek && docker rm trek +docker run -d --name trek \\ -p 3000:3000 \\ - -v /opt/nomad/data:/app/data \\ - -v /opt/nomad/uploads:/app/uploads \\ + -v /opt/trek/data:/app/data \\ + -v /opt/trek/uploads:/app/uploads \\ --restart unless-stopped \\ - mauriceboe/nomad:latest`} + mauriceboe/trek:latest`}