mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
fix(airtrail): gate airtrail update behind a user setting, on airtrail update: rebuild payload from fresh data to prevent any data loss
This commit is contained in:
@@ -489,7 +489,7 @@ export const addonsApi = {
|
|||||||
|
|
||||||
export const airtrailApi = {
|
export const airtrailApi = {
|
||||||
getSettings: () => apiClient.get('/integrations/airtrail/settings').then(r => r.data),
|
getSettings: () => apiClient.get('/integrations/airtrail/settings').then(r => r.data),
|
||||||
saveSettings: (data: { url: string; apiKey?: string; allowInsecureTls?: boolean }) =>
|
saveSettings: (data: { url: string; apiKey?: string; allowInsecureTls?: boolean; writeEnabled?: boolean }) =>
|
||||||
apiClient.put('/integrations/airtrail/settings', data).then(r => r.data),
|
apiClient.put('/integrations/airtrail/settings', data).then(r => r.data),
|
||||||
status: () => apiClient.get('/integrations/airtrail/status').then(r => r.data),
|
status: () => apiClient.get('/integrations/airtrail/status').then(r => r.data),
|
||||||
test: (data: { url?: string; apiKey?: string; allowInsecureTls?: boolean }) =>
|
test: (data: { url?: string; apiKey?: string; allowInsecureTls?: boolean }) =>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export default function AirTrailConnectionSection(): React.ReactElement {
|
|||||||
const [url, setUrl] = useState('')
|
const [url, setUrl] = useState('')
|
||||||
const [apiKey, setApiKey] = useState('')
|
const [apiKey, setApiKey] = useState('')
|
||||||
const [allowInsecureTls, setAllowInsecureTls] = useState(false)
|
const [allowInsecureTls, setAllowInsecureTls] = useState(false)
|
||||||
|
const [writeEnabled, setWriteEnabled] = useState(false)
|
||||||
const [connected, setConnected] = useState(false)
|
const [connected, setConnected] = useState(false)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
@@ -30,6 +31,7 @@ export default function AirTrailConnectionSection(): React.ReactElement {
|
|||||||
.then(d => {
|
.then(d => {
|
||||||
setUrl(d.url || '')
|
setUrl(d.url || '')
|
||||||
setAllowInsecureTls(!!d.allowInsecureTls)
|
setAllowInsecureTls(!!d.allowInsecureTls)
|
||||||
|
setWriteEnabled(!!d.writeEnabled)
|
||||||
setConnected(!!d.connected)
|
setConnected(!!d.connected)
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
@@ -46,7 +48,7 @@ export default function AirTrailConnectionSection(): React.ReactElement {
|
|||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
try {
|
try {
|
||||||
const d = await airtrailApi.saveSettings({ url: url.trim(), allowInsecureTls, ...keyPayload() })
|
const d = await airtrailApi.saveSettings({ url: url.trim(), allowInsecureTls, writeEnabled, ...keyPayload() })
|
||||||
const status = await airtrailApi.status().catch(() => ({ connected: false }))
|
const status = await airtrailApi.status().catch(() => ({ connected: false }))
|
||||||
setConnected(!!status.connected)
|
setConnected(!!status.connected)
|
||||||
setApiKey('')
|
setApiKey('')
|
||||||
@@ -107,6 +109,14 @@ export default function AirTrailConnectionSection(): React.ReactElement {
|
|||||||
<span className="text-sm font-medium text-slate-700">{t('settings.airtrail.allowInsecureTls')}</span>
|
<span className="text-sm font-medium text-slate-700">{t('settings.airtrail.allowInsecureTls')}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<ToggleSwitch on={writeEnabled} onToggle={() => setWriteEnabled(v => !v)} />
|
||||||
|
<span className="text-sm font-medium text-slate-700">{t('settings.airtrail.writeBack')}</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-xs text-slate-500">{t('settings.airtrail.writeBackHint')}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
|
|||||||
@@ -3045,6 +3045,15 @@ function runMigrations(db: Database.Database): void {
|
|||||||
'CREATE UNIQUE INDEX IF NOT EXISTS idx_reservations_external ON reservations(external_source, external_id, trip_id)',
|
'CREATE UNIQUE INDEX IF NOT EXISTS idx_reservations_external ON reservations(external_source, external_id, trip_id)',
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
() => {
|
||||||
|
// Per-user opt-in for writing TREK edits back to AirTrail (#1240). Default
|
||||||
|
// off: AirTrail is the source of truth and TREK never writes unless asked.
|
||||||
|
try {
|
||||||
|
db.exec('ALTER TABLE users ADD COLUMN airtrail_write_enabled INTEGER DEFAULT 0');
|
||||||
|
} catch (err: any) {
|
||||||
|
if (!err.message?.includes('duplicate column name')) throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (currentVersion < migrations.length) {
|
if (currentVersion < migrations.length) {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export class AirtrailController {
|
|||||||
body.url,
|
body.url,
|
||||||
body.apiKey,
|
body.apiKey,
|
||||||
!!body.allowInsecureTls,
|
!!body.allowInsecureTls,
|
||||||
|
!!body.writeEnabled,
|
||||||
getClientIp(req),
|
getClientIp(req),
|
||||||
);
|
);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ function airportCode(a: AirtrailAirport | null): string | null {
|
|||||||
* Airline/aircraft arrive as joined objects ({icao, iata, name, ...}); reduce
|
* Airline/aircraft arrive as joined objects ({icao, iata, name, ...}); reduce
|
||||||
* them to a single code (ICAO preferred, matching AirTrail's save shape).
|
* them to a single code (ICAO preferred, matching AirTrail's save shape).
|
||||||
*/
|
*/
|
||||||
function entityCode(e: AirtrailNamedCode | null | undefined): string | null {
|
export function entityCode(e: AirtrailNamedCode | null | undefined): string | null {
|
||||||
return e?.icao || e?.iata || null;
|
return e?.icao || e?.iata || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,14 +12,25 @@ interface UserConnRow {
|
|||||||
airtrail_url?: string | null;
|
airtrail_url?: string | null;
|
||||||
airtrail_api_key?: string | null;
|
airtrail_api_key?: string | null;
|
||||||
airtrail_allow_insecure_tls?: number | null;
|
airtrail_allow_insecure_tls?: number | null;
|
||||||
|
airtrail_write_enabled?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readRow(userId: number): UserConnRow | undefined {
|
function readRow(userId: number): UserConnRow | undefined {
|
||||||
return db
|
return db
|
||||||
.prepare('SELECT airtrail_url, airtrail_api_key, airtrail_allow_insecure_tls FROM users WHERE id = ?')
|
.prepare(
|
||||||
|
'SELECT airtrail_url, airtrail_api_key, airtrail_allow_insecure_tls, airtrail_write_enabled FROM users WHERE id = ?',
|
||||||
|
)
|
||||||
.get(userId) as UserConnRow | undefined;
|
.get(userId) as UserConnRow | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Has this user opted in to TREK writing their flight edits back to AirTrail? (#1240) */
|
||||||
|
export function isAirtrailWriteEnabled(userId: number): boolean {
|
||||||
|
const row = db.prepare('SELECT airtrail_write_enabled FROM users WHERE id = ?').get(userId) as
|
||||||
|
| { airtrail_write_enabled?: number | null }
|
||||||
|
| undefined;
|
||||||
|
return !!row?.airtrail_write_enabled;
|
||||||
|
}
|
||||||
|
|
||||||
/** Decrypted creds for outbound calls, or null when the user has no connection. */
|
/** Decrypted creds for outbound calls, or null when the user has no connection. */
|
||||||
export function getAirtrailCredentials(userId: number): AirtrailCreds | null {
|
export function getAirtrailCredentials(userId: number): AirtrailCreds | null {
|
||||||
const row = readRow(userId);
|
const row = readRow(userId);
|
||||||
@@ -40,6 +51,7 @@ export function getConnectionSettings(userId: number) {
|
|||||||
url: row?.airtrail_url || '',
|
url: row?.airtrail_url || '',
|
||||||
apiKeyMasked: row?.airtrail_api_key ? KEY_MASK : '',
|
apiKeyMasked: row?.airtrail_api_key ? KEY_MASK : '',
|
||||||
allowInsecureTls: !!row?.airtrail_allow_insecure_tls,
|
allowInsecureTls: !!row?.airtrail_allow_insecure_tls,
|
||||||
|
writeEnabled: !!row?.airtrail_write_enabled,
|
||||||
connected: !!(row?.airtrail_url && row?.airtrail_api_key),
|
connected: !!(row?.airtrail_url && row?.airtrail_api_key),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -49,6 +61,7 @@ export async function saveSettings(
|
|||||||
url: string | undefined,
|
url: string | undefined,
|
||||||
apiKey: string | undefined,
|
apiKey: string | undefined,
|
||||||
allowInsecureTls: boolean,
|
allowInsecureTls: boolean,
|
||||||
|
writeEnabled: boolean,
|
||||||
clientIp: string | null,
|
clientIp: string | null,
|
||||||
): Promise<{ success: boolean; warning?: string; error?: string }> {
|
): Promise<{ success: boolean; warning?: string; error?: string }> {
|
||||||
const trimmedUrl = (url || '').trim();
|
const trimmedUrl = (url || '').trim();
|
||||||
@@ -81,12 +94,12 @@ export async function saveSettings(
|
|||||||
|
|
||||||
if (newKey !== undefined) {
|
if (newKey !== undefined) {
|
||||||
db.prepare(
|
db.prepare(
|
||||||
'UPDATE users SET airtrail_url = ?, airtrail_api_key = ?, airtrail_allow_insecure_tls = ? WHERE id = ?',
|
'UPDATE users SET airtrail_url = ?, airtrail_api_key = ?, airtrail_allow_insecure_tls = ?, airtrail_write_enabled = ? WHERE id = ?',
|
||||||
).run(trimmedUrl || null, newKey, allowInsecureTls ? 1 : 0, userId);
|
).run(trimmedUrl || null, newKey, allowInsecureTls ? 1 : 0, writeEnabled ? 1 : 0, userId);
|
||||||
} else {
|
} else {
|
||||||
db.prepare(
|
db.prepare(
|
||||||
'UPDATE users SET airtrail_url = ?, airtrail_allow_insecure_tls = ? WHERE id = ?',
|
'UPDATE users SET airtrail_url = ?, airtrail_allow_insecure_tls = ?, airtrail_write_enabled = ? WHERE id = ?',
|
||||||
).run(trimmedUrl || null, allowInsecureTls ? 1 : 0, userId);
|
).run(trimmedUrl || null, allowInsecureTls ? 1 : 0, writeEnabled ? 1 : 0, userId);
|
||||||
// Clearing the URL with no key left makes the connection meaningless — drop the key too.
|
// Clearing the URL with no key left makes the connection meaningless — drop the key too.
|
||||||
if (!trimmedUrl) {
|
if (!trimmedUrl) {
|
||||||
db.prepare('UPDATE users SET airtrail_api_key = NULL WHERE id = ?').run(userId);
|
db.prepare('UPDATE users SET airtrail_api_key = NULL WHERE id = ?').run(userId);
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import {
|
|||||||
listFlights,
|
listFlights,
|
||||||
saveFlight,
|
saveFlight,
|
||||||
} from './airtrailClient';
|
} from './airtrailClient';
|
||||||
import { canonicalHash, mapFlightToReservation } from './airtrailMapper';
|
import { canonicalHash, entityCode, mapFlightToReservation } from './airtrailMapper';
|
||||||
import { getAirtrailCredentials } from './airtrailService';
|
import { getAirtrailCredentials, isAirtrailWriteEnabled } from './airtrailService';
|
||||||
|
|
||||||
/** Global on/off: the addon must be enabled and sync not explicitly turned off. */
|
/** Global on/off: the addon must be enabled and sync not explicitly turned off. */
|
||||||
export function syncGloballyEnabled(): boolean {
|
export function syncGloballyEnabled(): boolean {
|
||||||
@@ -144,7 +144,16 @@ function splitLocal(dt: string | null | undefined): { date: string | null; time:
|
|||||||
return { date: /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : null, time: m ? m[1] : null };
|
return { date: /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : null, time: m ? m[1] : null };
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSavePayload(reservation: any, existing: AirtrailFlightRaw): AirtrailSavePayload | null {
|
/**
|
||||||
|
* Build the POST /flight/save body. AirTrail's save fully overwrites the flight,
|
||||||
|
* so we start from the flight as AirTrail currently has it (`existing`, the raw
|
||||||
|
* GET object) and overwrite ONLY the fields TREK manages. Everything else —
|
||||||
|
* terminal, gate, scheduled/actual times, customFields, track, and any field
|
||||||
|
* AirTrail may add later — passes through untouched. We deliberately do NOT model
|
||||||
|
* those fields; spreading the raw object keeps us decoupled from AirTrail's schema
|
||||||
|
* (#1240).
|
||||||
|
*/
|
||||||
|
export function buildSavePayload(reservation: any, existing: AirtrailFlightRaw): AirtrailSavePayload | null {
|
||||||
let meta: Record<string, any>;
|
let meta: Record<string, any>;
|
||||||
try {
|
try {
|
||||||
meta = reservation.metadata ? JSON.parse(reservation.metadata) : {};
|
meta = reservation.metadata ? JSON.parse(reservation.metadata) : {};
|
||||||
@@ -183,7 +192,14 @@ function buildSavePayload(reservation: any, existing: AirtrailFlightRaw): Airtra
|
|||||||
if (ownSeat) ownSeat.seatNumber = seatNumber;
|
if (ownSeat) ownSeat.seatNumber = seatNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Spread the existing flight first to preserve every AirTrail-owned field, then
|
||||||
|
// overwrite only what TREK manages. `from`/`to`/`airline`/`aircraft` come back
|
||||||
|
// from GET as objects but the save shape wants codes — those are exactly the
|
||||||
|
// keys we override, so the spread never ships an object where a code is wanted.
|
||||||
return {
|
return {
|
||||||
|
// Cast so the spread carries through the AirTrail-owned keys we deliberately
|
||||||
|
// don't model (terminal, gate, scheduled/actual times, customFields, track, …).
|
||||||
|
...(existing as unknown as Record<string, unknown>),
|
||||||
id: Number(reservation.external_id),
|
id: Number(reservation.external_id),
|
||||||
from: fromCode,
|
from: fromCode,
|
||||||
to: toCode,
|
to: toCode,
|
||||||
@@ -191,14 +207,18 @@ function buildSavePayload(reservation: any, existing: AirtrailFlightRaw): Airtra
|
|||||||
departureTime: dep.time,
|
departureTime: dep.time,
|
||||||
arrival: arr.date,
|
arrival: arr.date,
|
||||||
arrivalTime: arr.time,
|
arrivalTime: arr.time,
|
||||||
airline: meta.airline ?? null,
|
// These are AirTrail-owned details TREK doesn't surface in its edit UI — a TREK
|
||||||
flightNumber: meta.flight_number ?? null,
|
// edit can leave them out of `metadata`. Preserve AirTrail's current value when
|
||||||
aircraft: meta.aircraft ?? null,
|
// TREK has none rather than nulling it out (#1240). entityCode mirrors the
|
||||||
aircraftReg: meta.aircraft_reg ?? null,
|
// import/hash code-selection so a writeback stays a no-op for the hash.
|
||||||
flightReason: meta.flight_reason ?? null,
|
airline: meta.airline ?? entityCode(existing.airline) ?? null,
|
||||||
note: reservation.notes ?? null,
|
flightNumber: meta.flight_number ?? existing.flightNumber ?? null,
|
||||||
|
aircraft: meta.aircraft ?? entityCode(existing.aircraft) ?? null,
|
||||||
|
aircraftReg: meta.aircraft_reg ?? existing.aircraftReg ?? null,
|
||||||
|
flightReason: meta.flight_reason ?? existing.flightReason ?? null,
|
||||||
|
note: reservation.notes ?? existing.note ?? null,
|
||||||
seats,
|
seats,
|
||||||
};
|
} as AirtrailSavePayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -219,9 +239,12 @@ export async function pushReservationToAirtrail(reservationId: number, tripId: n
|
|||||||
| undefined;
|
| undefined;
|
||||||
if (!row || !row.sync_enabled) return;
|
if (!row || !row.sync_enabled) return;
|
||||||
|
|
||||||
const creds: AirtrailCreds | null = row.external_owner_user_id
|
// AirTrail is read-only by default (#1240). Only push when the flight's owner has
|
||||||
? getAirtrailCredentials(row.external_owner_user_id)
|
// explicitly opted in. A no-op skip (not a detach): the link stays active so the
|
||||||
: null;
|
// inbound, AirTrail-wins pull keeps the reservation up to date.
|
||||||
|
if (!row.external_owner_user_id || !isAirtrailWriteEnabled(row.external_owner_user_id)) return;
|
||||||
|
|
||||||
|
const creds: AirtrailCreds | null = getAirtrailCredentials(row.external_owner_user_id);
|
||||||
if (!creds) {
|
if (!creds) {
|
||||||
detach(tripId, row.id); // owner disconnected — cannot push, so stop syncing
|
detach(tripId, row.id); // owner disconnected — cannot push, so stop syncing
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { buildSavePayload } from '../../../src/services/airtrail/airtrailSync';
|
||||||
|
import type { AirtrailAirport, AirtrailFlightRaw } from '../../../src/services/airtrail/airtrailClient';
|
||||||
|
|
||||||
|
function airport(over: Partial<AirtrailAirport> = {}): AirtrailAirport {
|
||||||
|
return {
|
||||||
|
id: 1,
|
||||||
|
icao: 'KJFK',
|
||||||
|
iata: 'JFK',
|
||||||
|
name: 'John F. Kennedy Intl.',
|
||||||
|
lat: 40.6413,
|
||||||
|
lon: -73.7781,
|
||||||
|
tz: 'America/New_York',
|
||||||
|
country: 'US',
|
||||||
|
...over,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An AirTrail flight as GET returns it, including the fields TREK doesn't model.
|
||||||
|
* Typed as the raw object (known shape + arbitrary passthrough keys) because the
|
||||||
|
* push spreads it wholesale rather than mapping each field — see buildSavePayload.
|
||||||
|
*/
|
||||||
|
function existingFlight(
|
||||||
|
over: Partial<AirtrailFlightRaw> & Record<string, unknown> = {},
|
||||||
|
): AirtrailFlightRaw & Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
id: 42,
|
||||||
|
from: airport(),
|
||||||
|
to: airport({ id: 2, icao: 'EGLL', iata: 'LHR', name: 'London Heathrow', tz: 'Europe/London' }),
|
||||||
|
date: '2021-09-01',
|
||||||
|
datePrecision: 'day',
|
||||||
|
departure: '2021-09-01T23:00:00.000+00:00',
|
||||||
|
arrival: '2021-09-02T07:00:00.000+00:00',
|
||||||
|
airline: { id: 1, icao: 'BAW', iata: 'BA', name: 'British Airways' },
|
||||||
|
flightNumber: 'BA178',
|
||||||
|
aircraft: { id: 1, icao: 'B772', name: 'Boeing 777' },
|
||||||
|
aircraftReg: 'G-VIIL',
|
||||||
|
flightReason: 'leisure',
|
||||||
|
note: 'window seat',
|
||||||
|
seats: [{ userId: 'u1', guestName: null, seat: 'window', seatNumber: '12A', seatClass: 'economy' }],
|
||||||
|
// AirTrail-owned detail TREK never surfaces — must survive a writeback (#1240).
|
||||||
|
departureScheduled: '2021-09-01',
|
||||||
|
departureScheduledTime: '18:45',
|
||||||
|
arrivalScheduled: '2021-09-02',
|
||||||
|
arrivalScheduledTime: '08:10',
|
||||||
|
takeoffActual: '2021-09-01',
|
||||||
|
takeoffActualTime: '19:12',
|
||||||
|
landingActual: '2021-09-02',
|
||||||
|
landingActualTime: '07:55',
|
||||||
|
departureTerminal: '7',
|
||||||
|
departureGate: 'B22',
|
||||||
|
arrivalTerminal: '5',
|
||||||
|
arrivalGate: 'A10',
|
||||||
|
customFields: { confirmation: 'ABC123' },
|
||||||
|
track: [{ lat: 40.6, lon: -73.7 }],
|
||||||
|
...over,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A linked TREK reservation (the shape getReservationWithJoins returns). */
|
||||||
|
function reservation(over: Record<string, unknown> = {}): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
external_id: '42',
|
||||||
|
reservation_time: '2021-09-01T19:00',
|
||||||
|
reservation_end_time: '2021-09-02T08:00',
|
||||||
|
notes: 'window seat',
|
||||||
|
metadata: JSON.stringify({ airline: 'BAW', flight_number: 'BA178', aircraft: 'B772', aircraft_reg: 'G-VIIL', flight_reason: 'leisure', seat: '12A' }),
|
||||||
|
endpoints: [
|
||||||
|
{ role: 'from', code: 'JFK' },
|
||||||
|
{ role: 'to', code: 'LHR' },
|
||||||
|
],
|
||||||
|
...over,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('airtrailSync.buildSavePayload', () => {
|
||||||
|
it('round-trips the AirTrail-owned fields TREK does not model (issue #1240)', () => {
|
||||||
|
const payload = buildSavePayload(reservation(), existingFlight());
|
||||||
|
expect(payload).not.toBeNull();
|
||||||
|
expect(payload).toMatchObject({
|
||||||
|
departureScheduled: '2021-09-01',
|
||||||
|
departureScheduledTime: '18:45',
|
||||||
|
arrivalScheduled: '2021-09-02',
|
||||||
|
arrivalScheduledTime: '08:10',
|
||||||
|
takeoffActual: '2021-09-01',
|
||||||
|
takeoffActualTime: '19:12',
|
||||||
|
landingActual: '2021-09-02',
|
||||||
|
landingActualTime: '07:55',
|
||||||
|
departureTerminal: '7',
|
||||||
|
departureGate: 'B22',
|
||||||
|
arrivalTerminal: '5',
|
||||||
|
arrivalGate: 'A10',
|
||||||
|
customFields: { confirmation: 'ABC123' },
|
||||||
|
track: [{ lat: 40.6, lon: -73.7 }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves a non-day date precision instead of resetting it to day', () => {
|
||||||
|
const payload = buildSavePayload(reservation(), existingFlight({ datePrecision: 'month' }));
|
||||||
|
expect(payload?.datePrecision).toBe('month');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still applies the TREK-owned edits on top of the preserved fields', () => {
|
||||||
|
const payload = buildSavePayload(
|
||||||
|
reservation({
|
||||||
|
reservation_time: '2021-09-01T20:30',
|
||||||
|
notes: 'changed in TREK',
|
||||||
|
metadata: JSON.stringify({ airline: 'BAW', flight_number: 'BA999', seat: '3C' }),
|
||||||
|
}),
|
||||||
|
existingFlight(),
|
||||||
|
);
|
||||||
|
expect(payload).toMatchObject({
|
||||||
|
id: 42,
|
||||||
|
from: 'JFK',
|
||||||
|
to: 'LHR',
|
||||||
|
departure: '2021-09-01',
|
||||||
|
departureTime: '20:30',
|
||||||
|
flightNumber: 'BA999',
|
||||||
|
note: 'changed in TREK',
|
||||||
|
});
|
||||||
|
// The user's seat number is pushed onto their own AirTrail seat.
|
||||||
|
expect(payload?.seats[0].seatNumber).toBe('3C');
|
||||||
|
// …without disturbing the preserved AirTrail detail.
|
||||||
|
expect(payload?.departureTerminal).toBe('7');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves AirTrail aircraft/airline/reason when TREK metadata omits them (#1240)', () => {
|
||||||
|
// A TREK edit can drop these AirTrail-owned fields from metadata; the writeback
|
||||||
|
// must fall back to AirTrail's current values rather than nulling them.
|
||||||
|
const payload = buildSavePayload(reservation({ metadata: JSON.stringify({}) }), existingFlight());
|
||||||
|
expect(payload).toMatchObject({
|
||||||
|
airline: 'BAW', // entityCode(existing.airline) — icao preferred
|
||||||
|
aircraft: 'B772',
|
||||||
|
aircraftReg: 'G-VIIL',
|
||||||
|
flightReason: 'leisure',
|
||||||
|
flightNumber: 'BA178',
|
||||||
|
note: 'window seat',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the existing seat manifest rather than replacing it', () => {
|
||||||
|
const payload = buildSavePayload(
|
||||||
|
reservation({ metadata: JSON.stringify({}) }),
|
||||||
|
existingFlight({
|
||||||
|
seats: [
|
||||||
|
{ userId: 'u1', guestName: null, seat: 'window', seatNumber: '12A', seatClass: 'business' },
|
||||||
|
{ userId: null, guestName: 'Guest', seat: 'aisle', seatNumber: '12B', seatClass: 'business' },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(payload?.seats).toHaveLength(2);
|
||||||
|
expect(payload?.seats[1]).toMatchObject({ guestName: 'Guest', seatNumber: '12B' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when an endpoint code is missing and no fallback exists', () => {
|
||||||
|
const payload = buildSavePayload(reservation({ endpoints: [] }), existingFlight({ from: airport({ iata: null, icao: null }) }));
|
||||||
|
expect(payload).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -21,6 +21,11 @@ export const airtrailSettingsSchema = z.object({
|
|||||||
apiKey: z.string().max(512).optional(),
|
apiKey: z.string().max(512).optional(),
|
||||||
/** Allow self-signed TLS certs (common on LAN instances). */
|
/** Allow self-signed TLS certs (common on LAN instances). */
|
||||||
allowInsecureTls: z.boolean().optional().default(false),
|
allowInsecureTls: z.boolean().optional().default(false),
|
||||||
|
/**
|
||||||
|
* Opt in to writing TREK edits back to AirTrail (#1240). Off by default:
|
||||||
|
* AirTrail is the source of truth and TREK only reads from it.
|
||||||
|
*/
|
||||||
|
writeEnabled: z.boolean().optional().default(false),
|
||||||
});
|
});
|
||||||
export type AirtrailSettings = z.infer<typeof airtrailSettingsSchema>;
|
export type AirtrailSettings = z.infer<typeof airtrailSettingsSchema>;
|
||||||
|
|
||||||
@@ -28,6 +33,7 @@ export const airtrailConnectionSchema = z.object({
|
|||||||
url: z.string(),
|
url: z.string(),
|
||||||
apiKeyMasked: z.string(),
|
apiKeyMasked: z.string(),
|
||||||
allowInsecureTls: z.boolean(),
|
allowInsecureTls: z.boolean(),
|
||||||
|
writeEnabled: z.boolean(),
|
||||||
connected: z.boolean(),
|
connected: z.boolean(),
|
||||||
});
|
});
|
||||||
export type AirtrailConnection = z.infer<typeof airtrailConnectionSchema>;
|
export type AirtrailConnection = z.infer<typeof airtrailConnectionSchema>;
|
||||||
|
|||||||
@@ -327,6 +327,8 @@ const settings: TranslationStrings = {
|
|||||||
'settings.airtrail.apiKeyHint': 'يُنشأ في AirTrail ضمن الإعدادات ← الأمان. يُخزَّن مشفّرًا.',
|
'settings.airtrail.apiKeyHint': 'يُنشأ في AirTrail ضمن الإعدادات ← الأمان. يُخزَّن مشفّرًا.',
|
||||||
'settings.airtrail.allowInsecureTls': 'السماح بالشهادات الموقّعة ذاتيًا',
|
'settings.airtrail.allowInsecureTls': 'السماح بالشهادات الموقّعة ذاتيًا',
|
||||||
'settings.airtrail.allowInsecureTlsHint': 'فعّل هذا فقط لنسخة موثوقة على شبكتك الخاصة.',
|
'settings.airtrail.allowInsecureTlsHint': 'فعّل هذا فقط لنسخة موثوقة على شبكتك الخاصة.',
|
||||||
|
'settings.airtrail.writeBack': 'كتابة التغييرات إلى AirTrail',
|
||||||
|
'settings.airtrail.writeBackHint': 'مُعطّل افتراضيًا: AirTrail هو مصدر الحقيقة وTREK يقرأ منه فقط. فعّله لإرسال التعديلات التي تجريها في TREK إلى AirTrail.',
|
||||||
'settings.airtrail.connected': 'متصل',
|
'settings.airtrail.connected': 'متصل',
|
||||||
'settings.airtrail.notConnected': 'غير متصل',
|
'settings.airtrail.notConnected': 'غير متصل',
|
||||||
'settings.airtrail.toast.saved': 'تم حفظ اتصال AirTrail',
|
'settings.airtrail.toast.saved': 'تم حفظ اتصال AirTrail',
|
||||||
|
|||||||
@@ -333,6 +333,8 @@ const settings: TranslationStrings = {
|
|||||||
'settings.airtrail.apiKeyHint': 'Gerada no AirTrail em Configurações → Segurança. Armazenada de forma criptografada.',
|
'settings.airtrail.apiKeyHint': 'Gerada no AirTrail em Configurações → Segurança. Armazenada de forma criptografada.',
|
||||||
'settings.airtrail.allowInsecureTls': 'Permitir certificados autoassinados',
|
'settings.airtrail.allowInsecureTls': 'Permitir certificados autoassinados',
|
||||||
'settings.airtrail.allowInsecureTlsHint': 'Ative apenas para uma instância confiável na sua própria rede.',
|
'settings.airtrail.allowInsecureTlsHint': 'Ative apenas para uma instância confiável na sua própria rede.',
|
||||||
|
'settings.airtrail.writeBack': 'Gravar alterações de volta no AirTrail',
|
||||||
|
'settings.airtrail.writeBackHint': 'Desativado por padrão: o AirTrail é a fonte da verdade e o TREK apenas lê dele. Ative para enviar ao AirTrail as alterações feitas no TREK.',
|
||||||
'settings.airtrail.connected': 'Conectado',
|
'settings.airtrail.connected': 'Conectado',
|
||||||
'settings.airtrail.notConnected': 'Não conectado',
|
'settings.airtrail.notConnected': 'Não conectado',
|
||||||
'settings.airtrail.toast.saved': 'Conexão com o AirTrail salva',
|
'settings.airtrail.toast.saved': 'Conexão com o AirTrail salva',
|
||||||
|
|||||||
@@ -334,6 +334,8 @@ const settings: TranslationStrings = {
|
|||||||
'settings.airtrail.apiKeyHint': 'Vygenerován v AirTrail v Nastavení → Zabezpečení. Uložen šifrovaně.',
|
'settings.airtrail.apiKeyHint': 'Vygenerován v AirTrail v Nastavení → Zabezpečení. Uložen šifrovaně.',
|
||||||
'settings.airtrail.allowInsecureTls': 'Povolit certifikáty podepsané sebou samým',
|
'settings.airtrail.allowInsecureTls': 'Povolit certifikáty podepsané sebou samým',
|
||||||
'settings.airtrail.allowInsecureTlsHint': 'Povolte pouze pro důvěryhodnou instanci ve vlastní síti.',
|
'settings.airtrail.allowInsecureTlsHint': 'Povolte pouze pro důvěryhodnou instanci ve vlastní síti.',
|
||||||
|
'settings.airtrail.writeBack': 'Zapisovat změny zpět do AirTrail',
|
||||||
|
'settings.airtrail.writeBackHint': 'Ve výchozím stavu vypnuto: AirTrail je zdrojem pravdy a TREK z něj pouze čte. Zapněte, chcete-li odesílat úpravy provedené v TREK zpět do AirTrail.',
|
||||||
'settings.airtrail.connected': 'Připojeno',
|
'settings.airtrail.connected': 'Připojeno',
|
||||||
'settings.airtrail.notConnected': 'Nepřipojeno',
|
'settings.airtrail.notConnected': 'Nepřipojeno',
|
||||||
'settings.airtrail.toast.saved': 'Připojení k AirTrail uloženo',
|
'settings.airtrail.toast.saved': 'Připojení k AirTrail uloženo',
|
||||||
|
|||||||
@@ -337,6 +337,8 @@ const settings: TranslationStrings = {
|
|||||||
'settings.airtrail.apiKeyHint': 'Wird in AirTrail unter Einstellungen → Sicherheit erstellt. Verschlüsselt gespeichert.',
|
'settings.airtrail.apiKeyHint': 'Wird in AirTrail unter Einstellungen → Sicherheit erstellt. Verschlüsselt gespeichert.',
|
||||||
'settings.airtrail.allowInsecureTls': 'Selbstsignierte Zertifikate erlauben',
|
'settings.airtrail.allowInsecureTls': 'Selbstsignierte Zertifikate erlauben',
|
||||||
'settings.airtrail.allowInsecureTlsHint': 'Nur für eine vertrauenswürdige Instanz im eigenen Netzwerk aktivieren.',
|
'settings.airtrail.allowInsecureTlsHint': 'Nur für eine vertrauenswürdige Instanz im eigenen Netzwerk aktivieren.',
|
||||||
|
'settings.airtrail.writeBack': 'Änderungen zurück nach AirTrail schreiben',
|
||||||
|
'settings.airtrail.writeBackHint': 'Standardmäßig aus: AirTrail ist die maßgebliche Quelle und TREK liest nur. Aktivieren, um in TREK vorgenommene Änderungen zurück an AirTrail zu senden.',
|
||||||
'settings.airtrail.connected': 'Verbunden',
|
'settings.airtrail.connected': 'Verbunden',
|
||||||
'settings.airtrail.notConnected': 'Nicht verbunden',
|
'settings.airtrail.notConnected': 'Nicht verbunden',
|
||||||
'settings.airtrail.toast.saved': 'AirTrail-Verbindung gespeichert',
|
'settings.airtrail.toast.saved': 'AirTrail-Verbindung gespeichert',
|
||||||
|
|||||||
@@ -326,6 +326,8 @@ const settings: TranslationStrings = {
|
|||||||
'settings.airtrail.apiKeyHint': 'Generated in AirTrail under Settings → Security. Stored encrypted.',
|
'settings.airtrail.apiKeyHint': 'Generated in AirTrail under Settings → Security. Stored encrypted.',
|
||||||
'settings.airtrail.allowInsecureTls': 'Allow self-signed certificates',
|
'settings.airtrail.allowInsecureTls': 'Allow self-signed certificates',
|
||||||
'settings.airtrail.allowInsecureTlsHint': 'Enable only for a trusted instance on your own network.',
|
'settings.airtrail.allowInsecureTlsHint': 'Enable only for a trusted instance on your own network.',
|
||||||
|
'settings.airtrail.writeBack': 'Write changes back to AirTrail',
|
||||||
|
'settings.airtrail.writeBackHint': 'Off by default: AirTrail is the source of truth and TREK only reads from it. Turn on to push edits made in TREK back to AirTrail.',
|
||||||
'settings.airtrail.connected': 'Connected',
|
'settings.airtrail.connected': 'Connected',
|
||||||
'settings.airtrail.notConnected': 'Not connected',
|
'settings.airtrail.notConnected': 'Not connected',
|
||||||
'settings.airtrail.toast.saved': 'AirTrail connection saved',
|
'settings.airtrail.toast.saved': 'AirTrail connection saved',
|
||||||
|
|||||||
@@ -334,6 +334,8 @@ const settings: TranslationStrings = {
|
|||||||
'settings.airtrail.apiKeyHint': 'Generada en AirTrail en Ajustes → Seguridad. Se almacena cifrada.',
|
'settings.airtrail.apiKeyHint': 'Generada en AirTrail en Ajustes → Seguridad. Se almacena cifrada.',
|
||||||
'settings.airtrail.allowInsecureTls': 'Permitir certificados autofirmados',
|
'settings.airtrail.allowInsecureTls': 'Permitir certificados autofirmados',
|
||||||
'settings.airtrail.allowInsecureTlsHint': 'Actívalo solo para una instancia de confianza en tu propia red.',
|
'settings.airtrail.allowInsecureTlsHint': 'Actívalo solo para una instancia de confianza en tu propia red.',
|
||||||
|
'settings.airtrail.writeBack': 'Escribir cambios de vuelta en AirTrail',
|
||||||
|
'settings.airtrail.writeBackHint': 'Desactivado por defecto: AirTrail es la fuente de referencia y TREK solo lee de él. Actívalo para enviar a AirTrail los cambios hechos en TREK.',
|
||||||
'settings.airtrail.connected': 'Conectado',
|
'settings.airtrail.connected': 'Conectado',
|
||||||
'settings.airtrail.notConnected': 'No conectado',
|
'settings.airtrail.notConnected': 'No conectado',
|
||||||
'settings.airtrail.toast.saved': 'Conexión con AirTrail guardada',
|
'settings.airtrail.toast.saved': 'Conexión con AirTrail guardada',
|
||||||
|
|||||||
@@ -339,6 +339,8 @@ const settings: TranslationStrings = {
|
|||||||
'settings.airtrail.apiKeyHint': 'Générée dans AirTrail sous Paramètres → Sécurité. Stockée chiffrée.',
|
'settings.airtrail.apiKeyHint': 'Générée dans AirTrail sous Paramètres → Sécurité. Stockée chiffrée.',
|
||||||
'settings.airtrail.allowInsecureTls': 'Autoriser les certificats auto-signés',
|
'settings.airtrail.allowInsecureTls': 'Autoriser les certificats auto-signés',
|
||||||
'settings.airtrail.allowInsecureTlsHint': 'À activer uniquement pour une instance de confiance sur votre propre réseau.',
|
'settings.airtrail.allowInsecureTlsHint': 'À activer uniquement pour une instance de confiance sur votre propre réseau.',
|
||||||
|
'settings.airtrail.writeBack': 'Réécrire les modifications dans AirTrail',
|
||||||
|
'settings.airtrail.writeBackHint': 'Désactivé par défaut : AirTrail fait référence et TREK se contente de le lire. Activez pour renvoyer vers AirTrail les modifications faites dans TREK.',
|
||||||
'settings.airtrail.connected': 'Connecté',
|
'settings.airtrail.connected': 'Connecté',
|
||||||
'settings.airtrail.notConnected': 'Non connecté',
|
'settings.airtrail.notConnected': 'Non connecté',
|
||||||
'settings.airtrail.toast.saved': 'Connexion AirTrail enregistrée',
|
'settings.airtrail.toast.saved': 'Connexion AirTrail enregistrée',
|
||||||
|
|||||||
@@ -340,6 +340,8 @@ const settings: TranslationStrings = {
|
|||||||
'settings.airtrail.apiKeyHint': 'Δημιουργείται στο AirTrail από Ρυθμίσεις → Ασφάλεια. Αποθηκεύεται κρυπτογραφημένο.',
|
'settings.airtrail.apiKeyHint': 'Δημιουργείται στο AirTrail από Ρυθμίσεις → Ασφάλεια. Αποθηκεύεται κρυπτογραφημένο.',
|
||||||
'settings.airtrail.allowInsecureTls': 'Να επιτρέπονται αυτο-υπογεγραμμένα πιστοποιητικά',
|
'settings.airtrail.allowInsecureTls': 'Να επιτρέπονται αυτο-υπογεγραμμένα πιστοποιητικά',
|
||||||
'settings.airtrail.allowInsecureTlsHint': 'Ενεργοποιήστε το μόνο για μια αξιόπιστη εγκατάσταση στο δικό σας δίκτυο.',
|
'settings.airtrail.allowInsecureTlsHint': 'Ενεργοποιήστε το μόνο για μια αξιόπιστη εγκατάσταση στο δικό σας δίκτυο.',
|
||||||
|
'settings.airtrail.writeBack': 'Εγγραφή αλλαγών πίσω στο AirTrail',
|
||||||
|
'settings.airtrail.writeBackHint': 'Απενεργοποιημένο από προεπιλογή: το AirTrail είναι η πηγή αλήθειας και το TREK μόνο διαβάζει από αυτό. Ενεργοποιήστε το για να στέλνετε πίσω στο AirTrail τις αλλαγές που κάνετε στο TREK.',
|
||||||
'settings.airtrail.connected': 'Συνδέθηκε',
|
'settings.airtrail.connected': 'Συνδέθηκε',
|
||||||
'settings.airtrail.notConnected': 'Δεν συνδέθηκε',
|
'settings.airtrail.notConnected': 'Δεν συνδέθηκε',
|
||||||
'settings.airtrail.toast.saved': 'Η σύνδεση με το AirTrail αποθηκεύτηκε',
|
'settings.airtrail.toast.saved': 'Η σύνδεση με το AirTrail αποθηκεύτηκε',
|
||||||
|
|||||||
@@ -336,6 +336,8 @@ const settings: TranslationStrings = {
|
|||||||
'settings.airtrail.apiKeyHint': 'Az AirTrailben a Beállítások → Biztonság menüpontban generálva. Titkosítva tárolva.',
|
'settings.airtrail.apiKeyHint': 'Az AirTrailben a Beállítások → Biztonság menüpontban generálva. Titkosítva tárolva.',
|
||||||
'settings.airtrail.allowInsecureTls': 'Önaláírt tanúsítványok engedélyezése',
|
'settings.airtrail.allowInsecureTls': 'Önaláírt tanúsítványok engedélyezése',
|
||||||
'settings.airtrail.allowInsecureTlsHint': 'Csak megbízható, saját hálózaton futó példány esetén engedélyezd.',
|
'settings.airtrail.allowInsecureTlsHint': 'Csak megbízható, saját hálózaton futó példány esetén engedélyezd.',
|
||||||
|
'settings.airtrail.writeBack': 'Módosítások visszaírása az AirTrailbe',
|
||||||
|
'settings.airtrail.writeBackHint': 'Alapértelmezés szerint kikapcsolva: az AirTrail a hiteles forrás, és a TREK csak olvas belőle. Kapcsold be, hogy a TREK-ben végzett módosításokat visszaküldje az AirTrailbe.',
|
||||||
'settings.airtrail.connected': 'Csatlakoztatva',
|
'settings.airtrail.connected': 'Csatlakoztatva',
|
||||||
'settings.airtrail.notConnected': 'Nincs csatlakoztatva',
|
'settings.airtrail.notConnected': 'Nincs csatlakoztatva',
|
||||||
'settings.airtrail.toast.saved': 'AirTrail-kapcsolat mentve',
|
'settings.airtrail.toast.saved': 'AirTrail-kapcsolat mentve',
|
||||||
|
|||||||
@@ -334,6 +334,8 @@ const settings: TranslationStrings = {
|
|||||||
'settings.airtrail.apiKeyHint': 'Dibuat di AirTrail pada Pengaturan → Keamanan. Disimpan terenkripsi.',
|
'settings.airtrail.apiKeyHint': 'Dibuat di AirTrail pada Pengaturan → Keamanan. Disimpan terenkripsi.',
|
||||||
'settings.airtrail.allowInsecureTls': 'Izinkan sertifikat yang ditandatangani sendiri',
|
'settings.airtrail.allowInsecureTls': 'Izinkan sertifikat yang ditandatangani sendiri',
|
||||||
'settings.airtrail.allowInsecureTlsHint': 'Aktifkan hanya untuk instans tepercaya di jaringanmu sendiri.',
|
'settings.airtrail.allowInsecureTlsHint': 'Aktifkan hanya untuk instans tepercaya di jaringanmu sendiri.',
|
||||||
|
'settings.airtrail.writeBack': 'Tulis perubahan kembali ke AirTrail',
|
||||||
|
'settings.airtrail.writeBackHint': 'Nonaktif secara bawaan: AirTrail adalah sumber kebenaran dan TREK hanya membaca darinya. Aktifkan untuk mengirim perubahan yang dibuat di TREK kembali ke AirTrail.',
|
||||||
'settings.airtrail.connected': 'Terhubung',
|
'settings.airtrail.connected': 'Terhubung',
|
||||||
'settings.airtrail.notConnected': 'Tidak terhubung',
|
'settings.airtrail.notConnected': 'Tidak terhubung',
|
||||||
'settings.airtrail.toast.saved': 'Koneksi AirTrail disimpan',
|
'settings.airtrail.toast.saved': 'Koneksi AirTrail disimpan',
|
||||||
|
|||||||
@@ -333,6 +333,8 @@ const settings: TranslationStrings = {
|
|||||||
'settings.airtrail.apiKeyHint': 'Generata in AirTrail in Impostazioni → Sicurezza. Memorizzata crittografata.',
|
'settings.airtrail.apiKeyHint': 'Generata in AirTrail in Impostazioni → Sicurezza. Memorizzata crittografata.',
|
||||||
'settings.airtrail.allowInsecureTls': 'Consenti certificati autofirmati',
|
'settings.airtrail.allowInsecureTls': 'Consenti certificati autofirmati',
|
||||||
'settings.airtrail.allowInsecureTlsHint': 'Abilita solo per un\'istanza attendibile sulla tua rete.',
|
'settings.airtrail.allowInsecureTlsHint': 'Abilita solo per un\'istanza attendibile sulla tua rete.',
|
||||||
|
'settings.airtrail.writeBack': 'Scrivi le modifiche su AirTrail',
|
||||||
|
'settings.airtrail.writeBackHint': 'Disattivato per impostazione predefinita: AirTrail è la fonte attendibile e TREK si limita a leggerlo. Attiva per inviare ad AirTrail le modifiche fatte in TREK.',
|
||||||
'settings.airtrail.connected': 'Connesso',
|
'settings.airtrail.connected': 'Connesso',
|
||||||
'settings.airtrail.notConnected': 'Non connesso',
|
'settings.airtrail.notConnected': 'Non connesso',
|
||||||
'settings.airtrail.toast.saved': 'Connessione AirTrail salvata',
|
'settings.airtrail.toast.saved': 'Connessione AirTrail salvata',
|
||||||
|
|||||||
@@ -313,6 +313,8 @@ const settings: TranslationStrings = {
|
|||||||
'settings.airtrail.apiKeyHint': 'AirTrail の「設定 → セキュリティ」で生成します。暗号化して保存されます。',
|
'settings.airtrail.apiKeyHint': 'AirTrail の「設定 → セキュリティ」で生成します。暗号化して保存されます。',
|
||||||
'settings.airtrail.allowInsecureTls': '自己署名証明書を許可する',
|
'settings.airtrail.allowInsecureTls': '自己署名証明書を許可する',
|
||||||
'settings.airtrail.allowInsecureTlsHint': '自分のネットワーク内の信頼できるインスタンスの場合にのみ有効にしてください。',
|
'settings.airtrail.allowInsecureTlsHint': '自分のネットワーク内の信頼できるインスタンスの場合にのみ有効にしてください。',
|
||||||
|
'settings.airtrail.writeBack': '変更を AirTrail に書き戻す',
|
||||||
|
'settings.airtrail.writeBackHint': '既定ではオフ: AirTrail が信頼できる情報源で、TREK は読み取りのみを行います。TREK で行った編集を AirTrail に書き戻すにはオンにします。',
|
||||||
'settings.airtrail.connected': '接続済み',
|
'settings.airtrail.connected': '接続済み',
|
||||||
'settings.airtrail.notConnected': '未接続',
|
'settings.airtrail.notConnected': '未接続',
|
||||||
'settings.airtrail.toast.saved': 'AirTrail の接続を保存しました',
|
'settings.airtrail.toast.saved': 'AirTrail の接続を保存しました',
|
||||||
|
|||||||
@@ -330,6 +330,8 @@ const settings: TranslationStrings = {
|
|||||||
'settings.airtrail.apiKeyHint': 'AirTrail의 설정 → 보안에서 생성됩니다. 암호화하여 저장됩니다.',
|
'settings.airtrail.apiKeyHint': 'AirTrail의 설정 → 보안에서 생성됩니다. 암호화하여 저장됩니다.',
|
||||||
'settings.airtrail.allowInsecureTls': '자체 서명 인증서 허용',
|
'settings.airtrail.allowInsecureTls': '자체 서명 인증서 허용',
|
||||||
'settings.airtrail.allowInsecureTlsHint': '자체 네트워크의 신뢰할 수 있는 인스턴스에서만 활성화하세요.',
|
'settings.airtrail.allowInsecureTlsHint': '자체 네트워크의 신뢰할 수 있는 인스턴스에서만 활성화하세요.',
|
||||||
|
'settings.airtrail.writeBack': '변경 사항을 AirTrail에 다시 기록',
|
||||||
|
'settings.airtrail.writeBackHint': '기본적으로 꺼져 있음: AirTrail이 신뢰할 수 있는 원본이며 TREK은 읽기만 합니다. TREK에서 변경한 내용을 AirTrail로 다시 보내려면 켜세요.',
|
||||||
'settings.airtrail.connected': '연결됨',
|
'settings.airtrail.connected': '연결됨',
|
||||||
'settings.airtrail.notConnected': '연결되지 않음',
|
'settings.airtrail.notConnected': '연결되지 않음',
|
||||||
'settings.airtrail.toast.saved': 'AirTrail 연결이 저장되었습니다',
|
'settings.airtrail.toast.saved': 'AirTrail 연결이 저장되었습니다',
|
||||||
|
|||||||
@@ -336,6 +336,8 @@ const settings: TranslationStrings = {
|
|||||||
'settings.airtrail.allowInsecureTls': 'Zelfondertekende certificaten toestaan',
|
'settings.airtrail.allowInsecureTls': 'Zelfondertekende certificaten toestaan',
|
||||||
'settings.airtrail.allowInsecureTlsHint':
|
'settings.airtrail.allowInsecureTlsHint':
|
||||||
'Schakel dit alleen in voor een vertrouwde instantie op je eigen netwerk.',
|
'Schakel dit alleen in voor een vertrouwde instantie op je eigen netwerk.',
|
||||||
|
'settings.airtrail.writeBack': 'Wijzigingen terugschrijven naar AirTrail',
|
||||||
|
'settings.airtrail.writeBackHint': 'Standaard uit: AirTrail is de bron van waarheid en TREK leest er alleen uit. Schakel in om in TREK gemaakte wijzigingen terug te sturen naar AirTrail.',
|
||||||
'settings.airtrail.connected': 'Verbonden',
|
'settings.airtrail.connected': 'Verbonden',
|
||||||
'settings.airtrail.notConnected': 'Niet verbonden',
|
'settings.airtrail.notConnected': 'Niet verbonden',
|
||||||
'settings.airtrail.toast.saved': 'AirTrail-verbinding opgeslagen',
|
'settings.airtrail.toast.saved': 'AirTrail-verbinding opgeslagen',
|
||||||
|
|||||||
@@ -335,6 +335,8 @@ const settings: TranslationStrings = {
|
|||||||
'settings.airtrail.apiKeyHint': 'Wygenerowany w AirTrail w sekcji Ustawienia → Bezpieczeństwo. Przechowywany w postaci zaszyfrowanej.',
|
'settings.airtrail.apiKeyHint': 'Wygenerowany w AirTrail w sekcji Ustawienia → Bezpieczeństwo. Przechowywany w postaci zaszyfrowanej.',
|
||||||
'settings.airtrail.allowInsecureTls': 'Zezwalaj na certyfikaty samopodpisane',
|
'settings.airtrail.allowInsecureTls': 'Zezwalaj na certyfikaty samopodpisane',
|
||||||
'settings.airtrail.allowInsecureTlsHint': 'Włącz tylko dla zaufanej instancji we własnej sieci.',
|
'settings.airtrail.allowInsecureTlsHint': 'Włącz tylko dla zaufanej instancji we własnej sieci.',
|
||||||
|
'settings.airtrail.writeBack': 'Zapisuj zmiany z powrotem w AirTrail',
|
||||||
|
'settings.airtrail.writeBackHint': 'Domyślnie wyłączone: AirTrail jest źródłem prawdy, a TREK tylko z niego odczytuje. Włącz, aby wysyłać zmiany wprowadzone w TREK z powrotem do AirTrail.',
|
||||||
'settings.airtrail.connected': 'Połączono',
|
'settings.airtrail.connected': 'Połączono',
|
||||||
'settings.airtrail.notConnected': 'Nie połączono',
|
'settings.airtrail.notConnected': 'Nie połączono',
|
||||||
'settings.airtrail.toast.saved': 'Zapisano połączenie z AirTrail',
|
'settings.airtrail.toast.saved': 'Zapisano połączenie z AirTrail',
|
||||||
|
|||||||
@@ -333,6 +333,8 @@ const settings: TranslationStrings = {
|
|||||||
'settings.airtrail.apiKeyHint': 'Создаётся в AirTrail в разделе «Настройки → Безопасность». Хранится в зашифрованном виде.',
|
'settings.airtrail.apiKeyHint': 'Создаётся в AirTrail в разделе «Настройки → Безопасность». Хранится в зашифрованном виде.',
|
||||||
'settings.airtrail.allowInsecureTls': 'Разрешить самоподписанные сертификаты',
|
'settings.airtrail.allowInsecureTls': 'Разрешить самоподписанные сертификаты',
|
||||||
'settings.airtrail.allowInsecureTlsHint': 'Включайте только для доверенного экземпляра в вашей собственной сети.',
|
'settings.airtrail.allowInsecureTlsHint': 'Включайте только для доверенного экземпляра в вашей собственной сети.',
|
||||||
|
'settings.airtrail.writeBack': 'Записывать изменения обратно в AirTrail',
|
||||||
|
'settings.airtrail.writeBackHint': 'По умолчанию выключено: AirTrail является источником истины, а TREK только читает из него. Включите, чтобы отправлять изменения, сделанные в TREK, обратно в AirTrail.',
|
||||||
'settings.airtrail.connected': 'Подключено',
|
'settings.airtrail.connected': 'Подключено',
|
||||||
'settings.airtrail.notConnected': 'Не подключено',
|
'settings.airtrail.notConnected': 'Не подключено',
|
||||||
'settings.airtrail.toast.saved': 'Подключение к AirTrail сохранено',
|
'settings.airtrail.toast.saved': 'Подключение к AirTrail сохранено',
|
||||||
|
|||||||
@@ -334,6 +334,8 @@ const settings: TranslationStrings = {
|
|||||||
'settings.airtrail.apiKeyHint': 'AirTrail\'de Ayarlar → Güvenlik altında oluşturulur. Şifreli olarak saklanır.',
|
'settings.airtrail.apiKeyHint': 'AirTrail\'de Ayarlar → Güvenlik altında oluşturulur. Şifreli olarak saklanır.',
|
||||||
'settings.airtrail.allowInsecureTls': 'Kendinden imzalı sertifikalara izin ver',
|
'settings.airtrail.allowInsecureTls': 'Kendinden imzalı sertifikalara izin ver',
|
||||||
'settings.airtrail.allowInsecureTlsHint': 'Yalnızca kendi ağınızdaki güvenilir bir örnek için etkinleştirin.',
|
'settings.airtrail.allowInsecureTlsHint': 'Yalnızca kendi ağınızdaki güvenilir bir örnek için etkinleştirin.',
|
||||||
|
'settings.airtrail.writeBack': 'Değişiklikleri AirTrail’e geri yaz',
|
||||||
|
'settings.airtrail.writeBackHint': 'Varsayılan olarak kapalı: AirTrail asıl kaynaktır ve TREK yalnızca okur. TREK’te yapılan değişiklikleri AirTrail’e geri göndermek için açın.',
|
||||||
'settings.airtrail.connected': 'Bağlandı',
|
'settings.airtrail.connected': 'Bağlandı',
|
||||||
'settings.airtrail.notConnected': 'Bağlı değil',
|
'settings.airtrail.notConnected': 'Bağlı değil',
|
||||||
'settings.airtrail.toast.saved': 'AirTrail bağlantısı kaydedildi',
|
'settings.airtrail.toast.saved': 'AirTrail bağlantısı kaydedildi',
|
||||||
|
|||||||
@@ -332,6 +332,8 @@ const settings: TranslationStrings = {
|
|||||||
'settings.airtrail.apiKeyHint': 'Згенеровано в AirTrail у розділі Налаштування → Безпека. Зберігається в зашифрованому вигляді.',
|
'settings.airtrail.apiKeyHint': 'Згенеровано в AirTrail у розділі Налаштування → Безпека. Зберігається в зашифрованому вигляді.',
|
||||||
'settings.airtrail.allowInsecureTls': 'Дозволити самопідписані сертифікати',
|
'settings.airtrail.allowInsecureTls': 'Дозволити самопідписані сертифікати',
|
||||||
'settings.airtrail.allowInsecureTlsHint': 'Вмикайте лише для довіреного екземпляра у вашій власній мережі.',
|
'settings.airtrail.allowInsecureTlsHint': 'Вмикайте лише для довіреного екземпляра у вашій власній мережі.',
|
||||||
|
'settings.airtrail.writeBack': 'Записувати зміни назад у AirTrail',
|
||||||
|
'settings.airtrail.writeBackHint': 'Типово вимкнено: AirTrail є джерелом істини, а TREK лише читає з нього. Увімкніть, щоб надсилати зміни, зроблені в TREK, назад до AirTrail.',
|
||||||
'settings.airtrail.connected': 'Підключено',
|
'settings.airtrail.connected': 'Підключено',
|
||||||
'settings.airtrail.notConnected': 'Не підключено',
|
'settings.airtrail.notConnected': 'Не підключено',
|
||||||
'settings.airtrail.toast.saved': 'Підключення AirTrail збережено',
|
'settings.airtrail.toast.saved': 'Підключення AirTrail збережено',
|
||||||
|
|||||||
@@ -319,6 +319,8 @@ const settings: TranslationStrings = {
|
|||||||
'settings.airtrail.apiKeyHint': '在 AirTrail 的「設定 → 安全性」中產生。以加密方式儲存。',
|
'settings.airtrail.apiKeyHint': '在 AirTrail 的「設定 → 安全性」中產生。以加密方式儲存。',
|
||||||
'settings.airtrail.allowInsecureTls': '允許自簽憑證',
|
'settings.airtrail.allowInsecureTls': '允許自簽憑證',
|
||||||
'settings.airtrail.allowInsecureTlsHint': '僅在你自己網路上受信任的執行個體啟用。',
|
'settings.airtrail.allowInsecureTlsHint': '僅在你自己網路上受信任的執行個體啟用。',
|
||||||
|
'settings.airtrail.writeBack': '將變更寫回 AirTrail',
|
||||||
|
'settings.airtrail.writeBackHint': '預設關閉:AirTrail 是資料來源,TREK 僅從中讀取。開啟後會將在 TREK 中所做的修改寫回 AirTrail。',
|
||||||
'settings.airtrail.connected': '已連接',
|
'settings.airtrail.connected': '已連接',
|
||||||
'settings.airtrail.notConnected': '未連接',
|
'settings.airtrail.notConnected': '未連接',
|
||||||
'settings.airtrail.toast.saved': '已儲存 AirTrail 連接',
|
'settings.airtrail.toast.saved': '已儲存 AirTrail 連接',
|
||||||
|
|||||||
@@ -318,6 +318,8 @@ const settings: TranslationStrings = {
|
|||||||
'settings.airtrail.apiKeyHint': '在 AirTrail 的“设置 → 安全”中生成。加密存储。',
|
'settings.airtrail.apiKeyHint': '在 AirTrail 的“设置 → 安全”中生成。加密存储。',
|
||||||
'settings.airtrail.allowInsecureTls': '允许自签名证书',
|
'settings.airtrail.allowInsecureTls': '允许自签名证书',
|
||||||
'settings.airtrail.allowInsecureTlsHint': '仅对您自己网络中受信任的实例启用。',
|
'settings.airtrail.allowInsecureTlsHint': '仅对您自己网络中受信任的实例启用。',
|
||||||
|
'settings.airtrail.writeBack': '将更改写回 AirTrail',
|
||||||
|
'settings.airtrail.writeBackHint': '默认关闭:AirTrail 是数据来源,TREK 仅从中读取。开启后会将在 TREK 中所做的修改写回 AirTrail。',
|
||||||
'settings.airtrail.connected': '已连接',
|
'settings.airtrail.connected': '已连接',
|
||||||
'settings.airtrail.notConnected': '未连接',
|
'settings.airtrail.notConnected': '未连接',
|
||||||
'settings.airtrail.toast.saved': 'AirTrail 连接已保存',
|
'settings.airtrail.toast.saved': 'AirTrail 连接已保存',
|
||||||
|
|||||||
Reference in New Issue
Block a user