fix(ci): the second round of the quick-win checks on the server

oidc_auto_redirect is managed-locked in the registry too, costs_mode is read
as the mode it is, the days of a dated trip are read through the factories,
and the dashboard's country order moves out of the atlas service into a
module of its own, so the service keeps its size.
This commit is contained in:
Maurice
2026-11-13 12:57:56 +01:00
parent c2ba78253d
commit 71cc97822a
9 changed files with 93 additions and 47 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
"src/db/repositories/Reservations.repository.ts": 1559,
"src/db/repositories/Trips.repository.ts": 1120,
"src/db/repositories/Users.repository.ts": 1445,
"src/nest/atlas/atlas.service.ts": 1177,
"src/nest/atlas/atlas.service.ts": 1174,
"src/nest/auth/auth.service.ts": 1143,
"src/nest/budget/budget.service.ts": 1607,
"src/nest/collections/collections.service.ts": 1414,
@@ -0,0 +1,42 @@
/**
* The visited countries in the order the dashboard shows them (#2648): most
* recently visited first, by the last day of the latest started trip that
* reached each one, so the few flags the tile has room for are the ones of the
* last journeys rather than the start of the alphabet. A country only marked by
* hand carries no date and follows, in the order it was marked.
*/
export class CountryVisits {
private readonly lastVisit = new Map<string, string>();
/** Keeps the latest trip day a country was reached on. */
note(code: string | null | undefined, tripLastDate: string | null): void {
if (!code) return;
const key = code.toUpperCase();
const when = tripLastDate ?? '';
const known = this.lastVisit.get(key);
if (known === undefined || when > known) this.lastVisit.set(key, when);
}
/** The reached countries, latest first, then the hand-marked ones not among them. */
ordered(manual: Iterable<string>): string[] {
// Array.prototype.sort is stable, so countries of the same trip keep the order they were found in.
const dated = [...this.lastVisit].sort(([, a], [, b]) => (a < b ? 1 : a > b ? -1 : 0)).map(([code]) => code);
return [...new Set([...dated, ...manual])];
}
}
/**
* One row per coordinate, the one of the latest trip through it: the country
* lookup is a point-in-polygon scan and runs once per coordinate.
*/
export function latestPerCoordinate<T extends { lat: number; lng: number; trip_last_date: string | null }>(
rows: T[],
): T[] {
const byCoord = new Map<string, T>();
for (const row of rows) {
const key = `${row.lat},${row.lng}`;
const known = byCoord.get(key);
if (!known || (row.trip_last_date ?? '') > (known.trip_last_date ?? '')) byCoord.set(key, row);
}
return [...byCoord.values()];
}
+6 -34
View File
@@ -40,6 +40,7 @@ import { transferEndpointIds } from './transfer-endpoints';
import { countryVisitDates } from './visit-dates';
import { placeCountryResolver } from './place-countries';
import { haversineKm } from '../common/geo';
import { CountryVisits, latestPerCoordinate } from './atlas-visit-order';
/** The part of a place that its cached region is derived from. */
type LocatedPlace = Pick<PlaceRow, 'id' | 'lat' | 'lng' | 'address'>;
@@ -70,19 +71,6 @@ function daysBetween(today: string, isoDate: string): number {
*/
type EndpointRow = TravelerOwnedEndpointRow;
/**
* The visited countries in the order the dashboard shows them (#2648): most
* recently visited first, by the last day of the latest started trip that
* reached each one, so the few flags the tile has room for are the ones of the
* last journeys rather than the start of the alphabet. A country only marked
* by hand carries no date and follows, in the order it was marked.
*/
export function countriesByLastVisit(lastVisit: ReadonlyMap<string, string>, manual: Iterable<string>): string[] {
// Array.prototype.sort is stable, so countries of the same trip keep the order they were found in.
const dated = [...lastVisit].sort(([, a], [, b]) => (a < b ? 1 : a > b ? -1 : 0)).map(([code]) => code);
return [...new Set([...dated, ...manual])];
}
export type CreateBucketData = {
name: string;
lat?: number | null;
@@ -1087,21 +1075,14 @@ export class AtlasService {
const manualCountries = (await this.visitedCountries.listCodesForUser(userId)) // AT43
.filter(Boolean)
.map(code => code.toUpperCase());
const lastVisit = new Map<string, string>();
const noteVisit = (code: string | null | undefined, tripLastDate: string | null) => {
if (!code) return;
const key = code.toUpperCase();
const when = tripLastDate ?? '';
const known = lastVisit.get(key);
if (known === undefined || when > known) lastVisit.set(key, when);
};
const visits = new CountryVisits();
// Only trips that have already started count as visited — a country you have merely
// booked a trip to isn't stamped in the passport yet, and one you jotted down without
// any dates even less so (#1048). `today` is UTC, matching tripVisitStatus.
const countryOf = placeCountryResolver();
for (const place of await this.placeRegions.listStartedTripPlaceCountriesForUser(userId, today)) { // AT44
noteVisit(countryOf(place, place.country_code), place.trip_last_date);
visits.note(countryOf(place, place.country_code), place.trip_last_date);
}
// Transport bookings don't create a place row, so their geocoded endpoints never
@@ -1120,17 +1101,8 @@ export class AtlasService {
// in the projection, so collapse here instead: getCountryFromCoords is a
// point-in-polygon scan and must stay at one run per coordinate. The latest
// trip through a coordinate is the one its country is dated by.
const endpointCoords = new Map<string, { lat: number; lng: number; tripLastDate: string | null }>();
for (const e of endpointRows) {
if (transfers.has(e.id)) continue;
const key = `${e.lat},${e.lng}`;
const known = endpointCoords.get(key);
if (!known || (e.trip_last_date ?? '') > (known.tripLastDate ?? '')) {
endpointCoords.set(key, { lat: e.lat, lng: e.lng, tripLastDate: e.trip_last_date });
}
}
for (const e of endpointCoords.values()) {
noteVisit(getCountryFromCoords(e.lat, e.lng), e.tripLastDate);
for (const e of latestPerCoordinate(endpointRows.filter(e => !transfers.has(e.id)))) {
visits.note(getCountryFromCoords(e.lat, e.lng), e.trip_last_date);
}
// Countries the user removed in Atlas stay removed on the dashboard too, so the
@@ -1138,7 +1110,7 @@ export class AtlasService {
const hidden = new Set([...(await this.getHiddenCountries(userId))].map(code => code.toUpperCase()));
return {
countries: countriesByLastVisit(lastVisit, manualCountries).filter(code => !hidden.has(code)),
countries: visits.ordered(manualCountries).filter(code => !hidden.has(code)),
cities: [...cities],
coords,
totalTrips: tripStats.trips || 0,
+2 -5
View File
@@ -308,8 +308,7 @@ export class AuthService {
const userCount = await this.usersRepo.countNonGuest();
const isDemo = readEnv().demo.enabled;
const toggles = await this.resolveAuthToggles();
// One directory deeper than the legacy src/services location — the extra
// '../' keeps resolving to the workspace package.json.
// One directory deeper than the old src/services, so the extra '../' still reaches the package.json.
const version: string = readEnv().app.appVersion ?? require('../../../package.json').version;
// Asked through the same resolver the search itself uses, so the client can
// never show Google features that rest on a key this caller does not get —
@@ -359,9 +358,7 @@ export class AuthService {
password_registration: isDemo ? false : toggles.password_registration,
oidc_login: toggles.oidc_login,
oidc_registration: isDemo ? false : toggles.oidc_registration,
// Read by the login page only where password login is off: anything but
// 'false' keeps the redirect every such instance had before (#2925).
oidc_auto_redirect: settings.get('oidc_auto_redirect') !== 'false',
oidc_auto_redirect: settings.get('oidc_auto_redirect') !== 'false', // read only without password login (#2925)
// Passkey login: the instance toggle + whether a usable RP ID resolves for
// this deployment. The login page shows the passkey button only when both
// are true. `passkey_configured` stays a pure boolean — it never leaks the
@@ -86,7 +86,7 @@ export const APP_SETTINGS = {
oidc_registration: { schema: flag, default: 'true', adminForm: true, managedLocked: true },
// Whether an instance without password login sends a visitor straight to the
// identity provider. Off shows the login page with its SSO button first (#2925).
oidc_auto_redirect: { schema: flag, default: 'true', adminForm: true, publicConfig: true },
oidc_auto_redirect: { schema: flag, default: 'true', adminForm: true, publicConfig: true, managedLocked: true },
passkey_login: { schema: flag, default: 'false', adminForm: true },
webauthn_rp_id: { schema: text, default: null, adminForm: true, managedLocked: true },
webauthn_origins: { schema: text, default: null, adminForm: true, managedLocked: true },
+3 -1
View File
@@ -34,6 +34,7 @@ import {
type DayGridRemoval,
type TrekWsPayload,
type TrekWsTripEventName,
type TripCostsMode,
type TripUpdateRequest,
} from '@trek/shared';
import { RealtimeService } from '../realtime/realtime.service';
@@ -473,7 +474,8 @@ export class TripsService {
const newCurrency = currency || trip.currency;
const newArchived = is_archived !== undefined ? (is_archived ? 1 : 0) : trip.is_archived;
const newCover = cover_image !== undefined ? cover_image : trip.cover_image;
const newCostsMode = costs_mode ?? trip.costs_mode;
// The column's CHECK holds a stored trip to the two modes.
const newCostsMode = costs_mode ?? (trip.costs_mode as TripCostsMode);
const oldReminder = (trip as any).reminder_days ?? 3;
const newReminder = reminder_days !== undefined
? (Number(reminder_days) >= 0 && Number(reminder_days) <= 30 ? Number(reminder_days) : oldReminder)
@@ -6,6 +6,7 @@
*/
import { db as testDb } from '../../../src/db/database';
import { BudgetItems } from '../../../src/db/entities/BudgetItems.entity';
import { Days } from '../../../src/db/entities/Days.entity';
import { ShareTokens } from '../../../src/db/entities/ShareTokens.entity';
import { TripMembers } from '../../../src/db/entities/TripMembers.entity';
import { Trips } from '../../../src/db/entities/Trips.entity';
@@ -15,7 +16,7 @@ import { NotificationsService } from '../../../src/nest/notifications/notificati
import { invalidatePermissionsCache } from '../../../src/nest/permissions/permissions-cache';
import { createUser, createAdmin, createTrip, addTripMember, createDayNote } from '../../helpers/factories';
import { addBudgetItemMember, addBudgetItemPayer, makeBudgetItem } from '../../helpers/factories/budget';
import { countRows, findRow, updateRows } from '../../helpers/factories/rows';
import { countRows, findRow, findRows, updateRows } from '../../helpers/factories/rows';
import { setAppSetting } from '../../helpers/factories/settings';
import { makeShareToken } from '../../helpers/factories/trips';
import { FakeRealtimeService } from '../../helpers/fake-realtime';
@@ -496,9 +497,7 @@ describe('Tool: export_trip_ics', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id, { title: 'Paris Trip', start_date: '2025-06-01', end_date: '2025-06-05' });
// A dated trip comes with its days; the first one is the 1st of June.
const day = testDb.prepare('SELECT id FROM days WHERE trip_id = ? AND day_number = 1').get(trip.id) as {
id: number;
};
const [day] = await findRows(orm, Days, { trip: trip.id }, { id: 'asc' });
createDayNote(testDb, day.id, trip.id, { text: 'Pick up the museum pass' });
await withHarness(user.id, async (h) => {
const call = async (args: Record<string, unknown>) =>
@@ -0,0 +1,34 @@
// The dashboard's order of visited countries (#2648).
import { CountryVisits, latestPerCoordinate } from '../../../src/nest/atlas/atlas-visit-order';
import { describe, it, expect } from 'vitest';
describe('CountryVisits', () => {
it('ATLAS-ORDER-001: puts the latest reached country first and the hand-marked ones after', () => {
const visits = new CountryVisits();
visits.note('pt', '2025-04-10');
visits.note('JP', '2026-05-02');
visits.note('pt', '2026-01-20');
visits.note(null, '2026-09-01');
expect(visits.ordered(['IS', 'JP'])).toEqual(['JP', 'PT', 'IS']);
});
it('ATLAS-ORDER-002: keeps the order a country of one trip was found in', () => {
const visits = new CountryVisits();
visits.note('FR', '2026-03-01');
visits.note('ES', '2026-03-01');
visits.note('IT', null);
expect(visits.ordered([])).toEqual(['FR', 'ES', 'IT']);
});
});
describe('latestPerCoordinate', () => {
it('ATLAS-ORDER-003: keeps one row per coordinate, the one of the latest trip', () => {
const rows = [
{ id: 1, lat: 1, lng: 2, trip_last_date: '2025-01-01' },
{ id: 2, lat: 1, lng: 2, trip_last_date: '2026-01-01' },
{ id: 3, lat: 3, lng: 4, trip_last_date: null },
];
expect(latestPerCoordinate(rows).map((r) => r.id)).toEqual([2, 3]);
});
});
@@ -1687,7 +1687,7 @@ describe('per-day summaries (#2602)', () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id, { title: 'Lisbon', start_date: '2025-06-02', end_date: '2025-06-03' });
// A dated trip comes with its days; the first one is the 2nd of June.
const day = testDb.prepare('SELECT id FROM days WHERE trip_id = ? AND day_number = 1').get(trip.id) as { id: number };
const [day] = await findRows(orm, Days, { trip: trip.id }, { id: 'asc' });
const untimed = createPlace(testDb, trip.id, { name: 'Untimed Spot' });
createDayAssignment(testDb, day.id, untimed.id);
createDayNote(testDb, day.id, trip.id, { text: 'Buy tram tickets' });