Merge branch 'dev' into test
This commit is contained in:
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -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
|
||||
|
||||
5
.github/workflows/test.yml
vendored
5
.github/workflows/test.yml
vendored
@@ -4,11 +4,6 @@ permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev]
|
||||
paths:
|
||||
- 'server/**'
|
||||
- '.github/workflows/test.yml'
|
||||
pull_request:
|
||||
branches: [main, dev]
|
||||
paths:
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -327,7 +327,7 @@ const es: Record<string, string> = {
|
||||
'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<string, string> = {
|
||||
// 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<string, string> = {
|
||||
'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<string, string> = {
|
||||
'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<string, string> = {
|
||||
'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<string, string> = {
|
||||
'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<string, string> = {
|
||||
'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',
|
||||
|
||||
@@ -1358,14 +1358,14 @@ export default function AdminPage(): React.ReactElement {
|
||||
<div style={{ marginTop: 14, padding: '12px 14px', borderRadius: 10, fontSize: 12, lineHeight: 1.8, fontFamily: 'monospace', whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}
|
||||
className="bg-gray-900 dark:bg-gray-950 text-gray-100 border border-gray-700"
|
||||
>
|
||||
{`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`}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 10, padding: '10px 12px', borderRadius: 10, fontSize: 12, lineHeight: 1.5 }}
|
||||
|
||||
@@ -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<string, unknown> | 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 = ?')
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -89,9 +89,15 @@ 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 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,12 +105,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).toBe(expectedStart);
|
||||
expect(res.body.trip.end_date).toBe(expectedEnd);
|
||||
|
||||
// Days with explicit dates should not be present
|
||||
// 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(0);
|
||||
expect(daysWithDate).toHaveLength(8);
|
||||
});
|
||||
|
||||
it('TRIP-001 — POST /api/trips requires a title, returns 400 without one', async () => {
|
||||
|
||||
Reference in New Issue
Block a user